-
Notifications
You must be signed in to change notification settings - Fork 3k
Apply a custom animation to ui-view during resolve #456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
The API provides you with two event hooks: |
But how can I tell if a state change applies to a particular ui-view instance without relying on/reinventing private implementation details of the view directive? Since the directive already has access to these, wouldn't it be easier if it exposed this capability? |
Okay, could you post some example code of how you would envision using this? Because implementing it as-described would mean all kinds of abstraction boundary violations. |
I envisage that, when a relevant state change starts, the If I have following element: <div class="main" ui-view="main"> Then during the resolve state of a relevant state change, the following CSS class would be applied: <div class="main ui-resolve" ui-view="main"> Which I could then use to apply a transparent overlay and loading spinner: .main.ui-resolve::after {
position: absolute;
content: '';
width: 100%;
height: 100%;
background: rgba(255,255,255,0.5) url('spinner.gif') no-repeat center center;
} When the resolve was finished, and the state change ended, the |
@jonrimmer you could do it, but "globally" - adding class to the body element no way to get the currently resolving state's element, afaik |
@stryju - That approach results in a horrible UI experience. The user clicks on a button to refresh a list or something, and the entire screen gets blanketed in a generic spinner because the library has no way of showing a spinner just for the view(s) that are actually affected by the state change. No thanks. In any case, after I filed this issue, I realised that the "resolve" feature is an anti-pattern. It waits for all the promises to resolve then animates the state change. This is completely wrong - you want your transition animations between states to run parallel to your data loads, so that the latter can be covered up by the former. For example, imagine your have a list of items, and clicking on one of them hides the list and shows the item's details in a different view. If we have an async load for the item details that takes, on average, 400ms, then we can cover up the load almost entirely in most cases by having a 300ms "leave" animation on the list view, and a 300ms "enter" animation on the item details view. That way we provide a slicker feel to the UI and can avoid showing a spinner at all in most cases. However, this requires that we initiate the async load and the state change animation at the same moment. If we use "resolve", then the entire async animation happens before the animation starts. The user clicks, sees a spinner, then sees the transition animation. The whole state change will take ~1000ms, which is too slow. "Resolve" could be a useful way to cache dependencies between different views if it had the option not to wait on promises, but the current behaviour, of always resolving them before the state change starts makes it almost useless, IMO. It should be avoided for any dependencies that involve async loads. |
@jonrimmer i totally agree here. there's no silver bullet here - the "resolve" pattern is powerful, but only if you know how to use it - otherwise you end up with what seems to be an "unresponsive" application :-) |
@stryju I still have a hard time to understand the point of Are the any plans to add this feature? Is there any workaround (besides moving the resolve logic to the controller)? |
@roberkules well, you COULD go with something like http://chieffancypants.github.io/angular-loading-bar/ - but it's global you potentially could use a pub/sub pattern and broadcast which view is about to get loaded, check if it has resolve set and then use something there |
I agree with @jonrimmer and @roberkules. Showing a loader animation only on the affected view while the resolve is happening is an important behavior that many developers would need to support (especially if they work with the type of designers that I've been working with). The solution that @jonrimmer proposes (i.e. adding a ui-resolve class to the affected view) would be an ideal solution. |
It's not an anti-pattern in itself, although the way people use it certainly can be. Perhaps some guidelines can be added to the documentation.
This is kind of a hard problem, since state transitions are intended to be atomic, and a failing resolve should fail the transition, in which case the state machine's job is to ensure that the UI is left in a consistent state. I'm open to opinions on how we can achieve both objectives. Regarding the specific problem of determining what views are affected at the outset of a state transition, I'm retooling ui-router's internals in a way that might make instrumenting this easier, but the fundamental design dictates one-way communication: What we might be able to do is have Would that solve it? |
@nateabele, your last suggestion (uiViews receiving and emitting notifications that state is about to change) would be a very welcome addition. It will allow us to offer feedback to the user that something is about to change on that view. I still think @jonrimmer's suggestion of adding a class (something like ui-resolve) to the impacted view is more desirable, because it will allow us to show a spinner at the right place at the right time without any code. Is it possible to do both? Even with just the solution you propose would be very valuable, as there currently isn't a way to do any of this at all. |
I'm going to second both @nateabele's and @johnnyoshika's suggestions. Both great ideas. Alternately, this may be a dirty word around these parts but, Ember's router forces you to load remote content by using the resolve pattern. The way it accomplishes this is that each Router (or state for ui-view) can have a loading function. When the router is resolving the data it will then execute this loading function, if it exists. If the loading function does not exist on the current route, it will then look at the parent route (if there is one) and execute it's loading function. Maybe ui.router can take a similar approach? A loading function can be executed, then when the state is ready to be shown (controller instantiated etc.) the content within the ui-view is blown away and replaced with the correct content? Additionally, i don't know how this will affect ng-animate integration. i'll leave that to someone with more knowledge than i. |
@Gwash3189, that's a really interesting suggestion and would work quite well. |
Yeah, I don't see why not.
Can you post some examples of how this works? |
The loading function that @Gwash3189 mentioned is described here: http://emberjs.com/guides/routing/loading-and-error-substates/ |
@johnnyoshika Thanks for that. @nateabele I'll have a go at it during lunch today. |
@nateabele Just to touch on a couple of things with the example.
I hope i touched on everything. If i missed anything, let me know. Edit |
I had another go at this problem. Again i didn't edit any ui router source. Loading.enter would run when $stateChangeStart is fired. This function displays a loading spinner. Implementing this was easy and is identical to the other plnkr i posted. The idea behind Loading.leave is that it would execute when the resolve is complete and the content (controller etc.) is ready to be instantiated. This gave me trouble and is still unimplemented as $stateChangeSuccess is broadcasted to late. Also, any other event, such as $viewContentLoaded does not send along the state it is transferring to, which makes finding any loading function impossible. I think the a valid solution to this problem is doing the following.
Please let me know your thoughts. I think this is a pretty solid solution as it gives the user hooks they can use (loading.enter & leave) and perform any thing during these phases and also provides ui-resolve-enter and ui-resolve-leave classes for css animations. |
@Gwash3189 what purpose does |
I think the existing API can work for this. By checking for a resolve property on the toState, and in deeper locations depending on your setup, you can show the loading only if there is a pending resolve.
|
I had the same issue discussed here. My app seemed to "unresponsive" due to a pending resolve. In my resolve I even call a remote resource, so there is always some loading time. I played around with some scenarios described here, but my case was even more severe: The resolving state is abstract, so there is never a stateChange event fired when it is loaded implicitly with its children. I came up with putting a full width loading animation within the root ui-view (not the ui-view of the abstract state) like this:
I find it a bit dirty, but it does its job. The div gets replaced when the abstract state is loaded. I hope this is useful for others. |
Or we could have something like
|
Here is a directive I am using for this. I just made it so hasn't been tested much. angular.module('myapp').directive('resolving', ['$rootScope', function($rootScope) {
return {
restrict: 'A',
link: function(scope, element, attributes) {
var mask = angular.element('<div class="resolving-mask"><img src="/assets/img/ajax-loader.gif" /></div>');
var showResolving = function() {
element.addClass('resolving');
element.append(mask);
};
var hideResolving = function() {
element.removeClass('resolving');
mask.remove();
};
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if ( ! toState.resolve) {
return;
}
// should be a resolve for a sub view
if (fromState.name.indexOf(attributes.resolving+'.') === 0 && toState.name.indexOf(attributes.resolving+'.') === 0) {
showResolving();
return;
}
// main resolving directive (no attribute name)
if ( ! attributes.resolving) {
// no fromState.name here means it should be first load, and no toState means redirecte
if ( ! fromState.name || ! toState) {
showResolving();
return;
}
// each root state is different
var fromStates = fromState.name.split('.');
var toStates = toState.name.split('.');
if ( ! fromStates[0] || ! toStates[0] || (fromStates[0] != toStates [0])) {
showResolving();
}
}
});
$rootScope.$on('$stateChangeSuccess', hideResolving);
$rootScope.$on('$stateChangeError', hideResolving);
}
};
}]); EDIT: someone (or I if I find time) could make this into a directive on ui-view instead and do what is requested in this issue. |
Why was this issue closed? Was it made possible to show a loader animation only on the affected view while the resolve is happening? |
@barboaz You may notice that this issue was reopened almost 4 months ago. |
@nateabele You're right, I misread the page. |
@timlind +1. Hit the nail on the head. Though I can see an argument for both behaviors, there is a strong case for making this possible. I tried having a crack at it myself but the ui-view directive is quite complex because of all of the cloning and removing of elements that's going on. |
I should also mention for the benefit of others that the only way I found around this was to manually handle animations and remove all of the resolvers and put them into the controller. That way the controllers resolve immediately and I just hide the new view until I'm ready. For more details see here: http://stackoverflow.com/questions/26510621/changing-how-the-ui-view-directive-handles-page-transitions |
@enrichit Thanks a lot for the input. We're in the process of a new version that will allow a lot more flexibility to handle this stuff. Could you just put together the different possible scenarios you want in a simple list of operations? Thanks. |
I will be glad if i can do scenario like this:
During this operation i see options: |
+1 for any sort of solution to this. Even the addition of a reference to the ui-view element being passed in view/state events would let us easily roll our own loading indicator solutions. In my mind the main problem now is not being able to differentiate between view containers in a complex layout, forcing us to resort to top-level loading indicators that provide a less than ideal experience. Integration with ng-animate would be a nice bonus, but I don't think it should be the only means provided as I know many developers prefer to use their own animation solutions. |
Hello guys! So what about this issue? Can anybody help me? Can i use this method for resolving our problem. angular.module('myapp').directive('resolving', ['$rootScope', function($rootScope) {
return {
restrict: 'A',
link: function(scope, element, attributes) {
var mask = angular.element('<div class="resolving-mask"><img src="/assets/img/ajax-loader.gif" /></div>');
var showResolving = function() {
element.addClass('resolving');
element.append(mask);
};
var hideResolving = function() {
element.removeClass('resolving');
mask.remove();
};
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if ( ! toState.resolve) {
return;
}
// should be a resolve for a sub view
if (fromState.name.indexOf(attributes.resolving+'.') === 0 && toState.name.indexOf(attributes.resolving+'.') === 0) {
showResolving();
return;
}
// main resolving directive (no attribute name)
if ( ! attributes.resolving) {
// no fromState.name here means it should be first load, and no toState means redirecte
if ( ! fromState.name || ! toState) {
showResolving();
return;
}
// each root state is different
var fromStates = fromState.name.split('.');
var toStates = toState.name.split('.');
if ( ! fromStates[0] || ! toStates[0] || (fromStates[0] != toStates [0])) {
showResolving();
}
}
});
$rootScope.$on('$stateChangeSuccess', hideResolving);
$rootScope.$on('$stateChangeError', hideResolving);
}
};
}]); i have 2 buttons in my menu header item:
|
Another mention of the same problem: https://www.bountysource.com/issues/1388153-don-t-wait-till-final-child-state-is-done-being-configured-in-order-to-load-parent-templates-and-controllers +1 At this point, resolves seem unusable with nested states and multiple views as they block the whole application. |
A fairly simple and generic solution would be determining currently resolved ui-view by element hierarchy.
This works quite well for simple state configurations with nested views. |
thank's! last example looks like good. |
My SO answer: My idea is to walk the path on state graph between transitioning states on The plunker demo introduces two root states: |
+1 |
@aikoven +1 Kudos, this helped tremendously. Tried everything I could find and nothing helped. I used a slight variation of this, but it solved the processing lag issue. The lag was not really evident until I bought an old Android phone to test with. If I can get the UX to work well on this old device, it'll work great on anything. |
+1 @AntonPOD your example worked great for simple ui view setups, as you said! |
Bleugh - just walked into this unfortunate resolve limitation. The behaviour I want is similar to @aikoven solution, but if I click on 'second.sub1' from 'first', I want the 'second' wrapper view to load immediately (as it only contains the section header), and the loader to only apply to 'second.sub1' (which happens to be a grid loading some data). As it is, I'm ditching the use of resolves in favour of loading data on a service on onEnter, and watching the service to see if it is loading / loaded (- my service explicitly provides a 'loading' variable that can be watched). |
@aikoven It seems like your solution is just a few steps away from using ng-enter/ng-leave semantics to enable ngAnimation capabilities. |
I solved the problem in my application by setting a flag in my controller when the state was changing and then hiding or showing a loading animation and my ui-view. If I was interested in which state was being transitioned from and to, that information is also available. Code from the controller: //Flag that indicates if the state is changing
vm.stateChanging = false;
//Call some code when a state change starts
$scope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
vm.stateChanging = true;
});
//Call some code when a state change finishes
$scope.$on("$stateChangeSuccess", function (event, toState, toParams, fromState, fromParams) {
vm.stateChanging = false;
}); The web page: <div ng-app="exampleApp" ng-controller="exampleController as example">
<div ng-hide="example.stateChanging" style="margin-top: 10px;">
<ui-view />
</div>
<div ng-show="example.stateChanging" style="margin-left: 10px;margin-top:10px;">
<img src="/Content/Images/Shared/spinner-large.gif" alt="Loading..." />
<span>Loading Page...</span>
</div>
</div> The loading animation displays whenever I change states and it gets replaced by the view after the resolve has finished and the new state is ready. |
@Maultasche, I've done the same thing in some places, the problem is just that it doesn't solve the case @PaddyMann is describing |
I know work was being done with this, but are we any closer to a solution for more complex routes? |
Is this something that is currently being implemented in a future version or would you welcome pull requests? |
👍 |
@joshuahiggins We haven't really come back to it yet. I think a PR might be a little premature, but if you want to take a look at our work on the feature-1.0 branch and propose a solution, we'd certainly be open to that. At this point, the 1.0 code is still pretty experimental, so I'd hesitate to put a lot of time into a solution before validating that it conforms to the current design vision. Hope that makes sense. Thanks for the interest. |
Closing this as it has a lot of stuff in it and a few workarounds. If someone wants to open a new issue for a feature request with a specific proposal, that would be greatly helpful. Like nate said, you can/should/please also try the new feature-1.0 branch that is in development to see if its any better. |
How would I show an animation before state is loaded (promise resolve)? |
you could attach it to the events: stateChangeStart and stateChangeSuccess |
Thank you @eddiemonge |
Here is a directive that allows simple control over showing loading states in the UI without abandoning pre-transition resolves usage in html:
implementation:
|
nice @william-mcmillian |
I've created a directive that will emit import angular from 'angular';
import 'angular-ui-router';
/**
* @ngdoc module
* @name uiViewLoader
*
* @description
* Emits the appropriate `uiViewLoaderReady`, `uiViewLoaderStart` and `uiViewLoaderStop` events on
* the `$rootScope`, passing them the respective `ui-view` element. This is to aid control over
* a specific `ui-view` when it is transitioning/resolving.
*/
const UiViewLoaderModule = angular.module('uiViewLoader', [
'ui.router'
])
.directive('uiViewLoader', () => ({
controller: UiViewLoaderController,
controllerAs: '$ctrl',
restrict: 'A'
}))
.constant('UI_VIEW_LOADER_EVENTS', {
UI_VIEW_LOADER_READY: 'uiViewLoaderReady',
UI_VIEW_LOADER_START: 'uiViewLoaderStart',
UI_VIEW_LOADER_STOP: 'uiViewLoaderStop',
STATE_CHANGE_START: '$stateChangeStart',
STATE_CHANGE_SUCCESS: '$stateChangeSuccess',
STATE_CHANGE_ERROR: '$stateChangeError',
STATE_NOT_FOUND: '$stateNotFound'
});
/*
* @private
* @ngdoc controller
* @name uiViewLoader.controller:UiViewLoaderController
*/
class UiViewLoaderController {
/**
* @param {JQLite} $element
* @param {Scope} $rootScope
* @param {Scope} $scope
* @param {Object} $attrs
* @param {function} $interpolate
* @param {Object.<string>} UI_VIEW_LOADER_EVENTS The events that this controller listens to
* or publishes
*/
constructor(
$element,
$rootScope,
$scope,
$attrs,
$interpolate,
UI_VIEW_LOADER_EVENTS
) {
'ngInject';
this._$element = $element;
this._$rootScope = $rootScope;
this._$scope = $scope;
this._$attrs = $attrs;
this._$interpolate = $interpolate;
this._events = UI_VIEW_LOADER_EVENTS;
}
$onInit() {
this._view = this._$interpolate(this._$attrs.uiView)(this._$scope);
this._state = this._$interpolate(this._$attrs.uiViewLoader)(this._$scope).split('.').pop();
this._viewRegex = new RegExp(`${this._view}@?`);
this.addUiRouterListeners();
this._$rootScope.$emit(this._events.UI_VIEW_LOADER_READY, this._$element);
}
$onDestroy() {
// make sure destroy happens after 'uiViewLoaderStop' has fired
const stopDestroy = this._$rootScope.$on(this._events.UI_VIEW_LOADER_STOP, () => {
this.removeUiRouterListeners();
stopDestroy();
});
}
/**
* Sets up listeners for uiRouter events
*/
addUiRouterListeners() {
this._listeners = [
this._events.STATE_CHANGE_START,
this._events.STATE_CHANGE_SUCCESS,
this._events.STATE_CHANGE_ERROR,
this._events.STATE_NOT_FOUND
].map(event => this._$rootScope.$on(event, this._onStateChange.bind(this)));
}
/**
* Removes listeners from uiRouter events
*/
removeUiRouterListeners() {
this._listeners.forEach(destroy => destroy());
}
/**
* Called when a uiRouter $stateChange* event is fired and emits the appropriate
* `uiViewLoaderStart` or `uiViewLoaderStop` events on the `$rootScope`.
*
* @param {Object} event The $statChange* event object.
* @param {Object} toState The state that the user is attempting to transition to.
*/
_onStateChange(event, toState, toParams, fromState) {
const { UI_VIEW_LOADER_START, UI_VIEW_LOADER_STOP } = this._events;
const segments = ['', ...toState.name.split('.')];
const index = segments.indexOf(this._state);
const hasView = toState.views && Object.keys(toState.views).some(view => view.match(this._viewRegex));
// isLoading if:
// - It's a page load/refresh so all states are loading at this point (`!fromState.name`)
// Or:
// - We're navigating to a state with a views object containing this view (hasView)
// Or:
// - We're navigating to a state that contains this state in it's name (`index > 1`)
// -- and it's a direct descendant of this state (`!segments[index + 2]`)
if (!fromState.name || hasView || (index > -1 && !segments[index + 2])) {
const isChangeStart = event.name === this._events.STATE_CHANGE_START;
const emit = isChangeStart ? UI_VIEW_LOADER_START : UI_VIEW_LOADER_STOP;
this._$rootScope.$emit(emit, this._$element, toState);
}
}
} To use it you need to add a $stateProvider
.state('root', {
abstract: true,
resolve: {
data: $timeout => $timeout(angular.noop, 1000);
}
})
.state('root.about', {
url: '/about',
resolve: {
data: $timeout => $timeout(angular.noop, 1000);
},
views: {
'main@': {
template: `
<h2>About</h2>
<div ui-view ui-view-loader="root.about"></div>
`
}
}
}); Doing it this way means that any state that loads as a direct descendant of the If you do not pass a state name it will assume I have created a codepen demonstrating how this can be used with your own custom If anyone can think of ways to improve this, please do share... 🙂 I should point out that you need Angular 1.5.5 for the above code to work (and an ES6 transpiler). |
It would be very useful if ui-view ran a custom "resolve" animation between the beginning and end of a state change that affects it. This could be used to display a spinner or other loading animation, which is vital when the resolve includes fetching things from the server.
The text was updated successfully, but these errors were encountered: