NG6-starter
NG6-starter copied to clipboard
How to use resolve
Guys, with this syntax:
$stateProvider
.state('home', {
url: '/',
template: '<home></home>'
});
It's possible to use resolve? Or do we need a controller property in the state in order to use it?
Thank you.
what do you mean by resolve? are you talking about
$stateProvider
.state('home', {
url: '/',
template: '<home></home>',
resolve: {
yourData: function(yourService) {
return yourService.getAsyncPromise()
}
}
});
@gdi2290 Yes, How is 'yourData' injected in the controller if we are rendering the component using template. For resolve to work do we need controller: 'HomeCtrl' and templateUrl: 'home.html', don't we? Sorry if I'm missing something.
@santios here's an example
client/app/components/home/home.js
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import homeComponent from './home.component';
let homeModule = angular.module('home', [
uiRouter
])
.config(($stateProvider, $urlRouterProvider)=>{
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url: '/',
template: '<home></home>',
resolve: {
'yourData': (yourService) => {
return yourService.getAsyncPromise()
}
}
});
})
.directive('home', homeComponent);
export default homeModule;
client/app/components/home/home.controller.js
class HomeController {
constructor(yourData) {
console.log(yourData)
this.name = 'home';
}
}
HomeController.$inject = ['yourData'];
export default HomeController;
@gdi2290 Thank you for your answer but this is throwing and error: "Unknown provider: myDataProvider"
I think the problem is that you can't inject dependencies to this controller, as the controller is being used inside a directive definition, and we are rendering the component directly in the template option of the state (template: '<home></home>').
import template from './home.html';
import controller from './home.controller';
import './home.styl';
let homeComponent = function(){
return {
template,
controller, //this is home controller, how will resolve inject the data here?
restrict: 'E',
controllerAs: 'vm',
scope: {},
bindToController: true
};
};
I think we need a folder called pages, where we use components. For example:
$stateProvider
.state('home', {
url: '/',
templateUrl: 'page.html',
controller: PageController,
resolve: {
myData: function(){
return promise;
}
}
});
And inside page.html we can use the home component and posible pass the resolved data:
<home myData='ctrl.myData'></home>
What do you think?
$stateProvider
.state('home', {
url: '/',
templateUrl: 'page.html',
controller: PageController,
resolve: {
myData: function(){
return promise;
}
}
});
you need to inject your service and return either an object or a promise and this won't work when dealing with homeComponent since it's a directive
@gdi2290 I have exactly same issue with resolve as @santios so what is the proper solution for this problem?
@martinmicunda this is solved in angular2 but you would handle it like this in a directive
import template from './home.html';
import controller from './home.controller';
import './home.styl';
let homeComponent = function(){
return {
template,
controller, //this is home controller, how will resolve inject the data here?
restrict: 'E',
controllerAs: 'vm',
scope: {},
bindToController: true
};
};
home.controller.js
class HomeController {
constructor(YourService) {
console.log(YourService);
this.yourData = [];
// resolve data
YourService.getAsyncPromise().then(res => {
this.yourData = res.yourData;
});
this.name = 'home';
}
}
HomeController.$inject = ['YourService'];
export default HomeController;
the resolve is a way for us to synchronously load our data before we load our template. With the directive we don't really have that luxury without wrapping the directive
@gdi2290 yeah that's what I start doing but then I got more complex example with onEnter and that doesn't work either...
$stateProvider
.state('employees.add', {
url: '/add',
onEnter: function($stateParams, $state, $modal) {
$modal.open({
template: template,
resolve: {
languages: LanguageResource => LanguageResource.getList(),
positions: PositionResource => PositionResource.getList({lang: 'en'}),
roles: RoleResource => RoleResource.getList({lang: 'en'})
},
controller: 'EmployeesAddController',
controllerAs: 'vm',
size: 'lg'
}).result.finally(function() {
$state.go('employees');
});
}
});
@martinmicunda You won't be able to use much more than the template and controller option inside the state object. If you really want to do this, you should create a pages folder ( that will bring up some duplication) and create there a plain controller with a view using the components, something like this:
pages/main/
main.controller.js
main.js
main.html
Inside main.html:
<home></home>
And in main.js you can use the normal resolve with all the options you are used too.
There is this solution that keeps both resolve and component in tact:
in home.js
$stateProvider
.state('home', {
url: '/',
controller: function($scope, yourData) {
this.yourData = yourData;
},
controllerAs: 'homeState',
template: '<home your-data="homeState.yourData"></home>',
resolve: {
yourData: function() {
return 42;
}
}
});
in home.component.js
let homeComponent = function(){
return {
template,
controller,
restrict: 'E',
controllerAs: 'vm',
scope: {
yourData: '='
},
bindToController: true
};
};
and in home.controller.js
class HomeController {
constructor(){
this.name = 'home';
this.data = this.yourData;
}
}
A little bit of boilerplate (that can be customized in the generator's templates) and an additional controller for each component generated, but that does the trick.
@eshcharc That's clever, thank you for sharing.
@santios If you have a spare time, please open a PR.
to resolve you need to
- create resolves in state;
- add a controller to the state
- inject the data that you want to resolve in the controller
- bind the data to your directive in the template
- configure your directive to receive data
this works since the template won't load until resolve is finished then it's only a problem of passing the data to the directive
+1. I lost a good few hours of my life trying to fix this in a more elegant way and the page solution feels more palatable than the everything is a directive approach.
Hi @eshcharc (Ma Kore? :) I've tried your solution 1 for 1, no typos or anything, and for some reason my controller doesn't receive the props i'm binding in the template. Any idea why?
Ok Ok got it! Not sure why, but my component needed to look a little bit different then your's:
let categoryComponent = {
restrict: 'E',
template,
controller,
controllerAs: 'vm',
bindings: {
categoryData: '='
}
};
Glad you could solve that. Next time, don't esitate to call. Since I started using RxJs I find it rare that I inject to component. I rather subscribe to the proper stream. Try that, it'll change your programmatic life...
In due time :-) thanks!
@eshcharc can you provide an example?
It's not about an example. You will need to read and see what RxJs is all about. The thing is that you set your model as a stream and register for changes in your componet. This is quite out of scope here.
@eshcharc I know about reactive programming, I only doesn't thought about representing state as data stream (in angular components context)
State and data manipulation is best achieved with Scan operator.
@eshcharc your solution is totally insane!!!! really helped me a lot :) thanks for share it, really appreciate it.
@santios @gdi2290 according to the angularjs 1.5.0-rc.0 docs this is how you resolve data for a component:
var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
template: '<h1>Home</h1><p>Hello, {{ home.user.name }} !</p>',
bindings: {user: '='}
});
myMod.config(function($routeProvider) {
$routeProvider.when('/', {
template: '<home user="$resolve.user"></home>',
resolve: {user: function($http) { return $http.get('...'); }}
});
});
hope it works for somebody
@blackendstudios but that is ngRoute, this project uses ui-router
@wormyy yeah yeah,I know, is for illustration porpuses, I that's way I said "@santios @gdi2290 according to the angularjs 1.5.0-rc.0 docs this is how you resolve data for a component"
Sorry guys, I still don't get it.
After running "gulp component --name admin" in my CMD I got this in my admin.component.js:
import template from './admin.html';
import controller from './admin.controller';
import './admin.styl';
let adminComponent = {
restrict: 'E',
bindings: {},
template,
controller,
controllerAs: 'vm'
};
export default adminComponent;
and this in my admin.js:
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import adminComponent from './admin.component';
import {default as AdminController} from './admin.controller';
let adminModule = angular.module('admin', [
uiRouter
]).component('admin', adminComponent);
export default adminModule;
For triggering resolve in ui-router I need to change admin.js to this:
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import adminComponent from './admin.component';
import {default as AdminController} from './admin.controller';
let adminModule = angular.module('admin', [
uiRouter
])
.config(($stateProvider, $urlRouterProvider) => {
"ngInject";
$urlRouterProvider.otherwise('/');
$stateProvider
.state('admin', {
url: '/admin',
template: '<admin></admin>',
controller: AdminController,
controllerAs: 'vm',
resolve: {
retailersList: ['RestManager', 'AuthManager', (rest, auth) => {
if (auth.isLogin() && auth.isAdmin())
return rest.getRetailersList();
return [];
}]
}
});
})
.component('admin', adminComponent);
export default adminModule;
but now my admin.controller.js file is getting invoked twice and that can't be good!
In the second invocation I'm getting an error: Unknown provider: retailersListProvider <- retailersList
This is my admin.controller.js file:
let vm = null;
class AdminController {
constructor(retailersList) {
vm = this;
vm.retailersList = retailersList;
}
}
AdminController.$inject = ['retailersList'];
export default AdminController;
I'm sure I can have all kind of workarounds but what is the best practice?
Thank you.
O.K. I got it: All I needed to do is replace the templates in the admin.js file and the admin.components.js file like this:
in the admin.js file switch this line:
template: template,
with this line:
template: '<admin></admin>',
and in the admin.components.js file switch this line:
template: '<admin></admin>',
to this line:
template: template,
Also add
import template from './admin.html';
to the top of the admin.js file and delete the same line from admin.component.js file.
but now my admin.controller.js file is getting invoked twice and that can't be good!
What do you expected? You have one controller in state definition and one in component.
what is the best practice?
Well... ok. First of all, data should be passed to component via bindings. (also all your components (i.e. custom elements) should have prefix.)
import template from './admin.html';
import './admin.styl';
// I like to make all components controllers private
// but you chose how to work with them
class MyAdminComponent {
// this is instead of watchers in controllers
set list(list) {
this._list = list;
this.reactOnListChanges();
}
reactOnListChanges() {
// do stuff...
}
}
export default {
restrict: 'E',
bindings: {
list: '='
},
template,
controllerAs: 'vm'
};
This is our component. All state which it need will be passed from above via bindings. And this state will be prepared in our route resolvers. One note, consider to move all your resolvers to separate resolver-services.
export function retailersListResolver(rest, auth) {
"ngInject";
if (auth.isLogin() && auth.isAdmin()) {
return rest.getRetailersList();
}
return [];
}
// admin.js or somewhere else
import * as resolvers from './resolvers';
angular
.module('app')
.service(resolvers); // this will register all your resolvers
.config(function ($stateProvider) {
$stateProvider.state('admin', {
url: '/admin',
resolves: {
'retailersList': 'retailersListResolver' // service instance
},
// we need to aggregate resolved values
// in uiRouter 0.2.19 this will be done automaticly
controller: function ($scope, retailersList) {
$scope.$resolve = {retailersList};
},
// now we will pass data to our component
template: `<my-admin list="$resolve.retailersList"></my-admin>`,
}
});
About uiRouter 0.2.19:
controller: function ($scope, retailersList) {
$scope.$resolve = {retailersList};
}
You can update uiRouter to latest version (i.e. 0.2.19-dev) to get rid of this. This was implemented 2-3 weeks ago and merged into legacy branch.
Does that helped?
That solution looks neat! Thank you.
@eshcharc Hi, first of all, thanks for your solution I lost a few hours before I found this issue, I'm sorry to bother you but I tried your solution and it doesn't work for me :( I git cloned the project and started fresh ! I added your solution like that with some debugging
$stateProvider
.state('home', {
url: '/',
controller: ($scope, yourData) => {
console.log("1 " + yourData);
console.log(this);
this.yourData = yourData;
console.log("2 " + this.yourData);
},
controllerAs: 'homeState',
template: '<home your-data="homeState.yourData"></home>',
resolve: {
yourData: () => { console.log('resolving'); return 42; }
}
});
and in the controller this is undefined.
Am I missing something ?