Open santios opened 9 years ago
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
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 ?
@pibouu Hi!
Am I missing something ?
Yep, read about arrow functions and lexical this. This will answer your question. Change it like this:
{
controller: ($scope, yourData) => {
$scope.$resolve = {yourData};
},
// controllerAs: '$ctrl' - no need for this since we are using scopes
template: `<my-home your-data="$resolve.yourData"></my-home>`
}
As I said earlier, ngRoute already support automatic population of $resolve
scope property, and uiRouter already have it in master branch (0.2.19-dev).
@fesor you should use array dependency annotation
{
controller:['$scope', 'yourData', ($scope, yourData) => {
$scope.$resolve = {yourData};
}],
// controllerAs: '$ctrl' - no need for this since we are using scopes
template: `<my-home your-data="$resolve.yourData"></my-home>`
}
@fesor @blackendstudios Thanks for your replies thank you so much ! ! It works fine now !
ps: I used the legacy branch with the 0.2.18 of ui-router.
@blackendstudios I use latest version of uiRouter, so i just don't need controller at all (https://github.com/angular-ui/ui-router/commit/0f6aea62a3e92b99b892aa062d1f3be8e5bafa6a). But yes, you are right.
@fesor 0.2.19-dev
not exist. 1.0.0-alpha.4
at that moment
Hi guys,
if someone is still interesting, I made it work using ui-router 1.0.0-alpha.5, and following ToddMotto styleguide.
and this is how the code shoud look like
/* ----- todo/todo.component.js ----- */
import template from './todo.html';
import controller from './todo.controller';
const TodoComponent = {
bindings: {
todoData: '<'
},
controller,
template: template
};
export default TodoComponent;
/* ----- todo/todo.controller.js ----- */
class TodoController {
constructor() {
"ngInject";
console.log('this is the data from resolve', this.todoData);
}
}
export default TodoController;
/* ----- todo/index.js ----- */
import angular from 'angular';
import TodoComponent from './todo.component';
const todo = angular
.module('todo', [])
.component('todo', TodoComponent)
.config(($stateProvider, $urlRouterProvider) => {
"ngInject";
$stateProvider
.state('todos', {
url: '/todos',
component: 'todo',
resolve: {
todoData: PeopleService => PeopleService.getAllPeople();
}
});
$urlRouterProvider.otherwise('/');
})
.name;
export default todo;
hope this help.
How can you test state resolves with this approach? I've been trying various approaches and nothing is working. Any help appreciated :)
EG my state resolve:
$stateProvider
.state('someState', {
url: '/p/:id',
resolve: {
someData: (myService, $stateParams) => {
return myService.getTheData($stateParams.id);
}
},
controller: ['$scope', 'someData', ($scope, someData) => {
$scope.profile = someData.data;
}],
template: '<some-page profile="someData"></some-page>'
});
simplified spec:
beforeEach(function(){
window.module(Services.name);
window.module(($provide) => {
$provide.value('profile', { name: 'testing' } );
});
});
beforeEach(inject((_$rootScope_, _$q_, _$httpBackend_, myService) => {
$rootScope = _$rootScope_;
$q = _$q_;
deferred = _$q_.defer();
$httpBackend = _$httpBackend_;
myService = myService;
scope = $rootScope.$new();
scope.vm = $rootScope.$new();
spyOn(myService, 'getTheData').and.callThrough();
makeController = () => {
return new MyController(myService, {$scope: scope, profile: mockProfile });
};
}));
describe('Controller', () => {
it('should have a name property', () => {
// let vm = makeController(myService, {$scope: scope, profile: mockProfile});
let vm = makeController();
expect(vm.profile.name).toBeDefined();
});
});
@gdi2290 any tips?
@ttbarnes #199
You just shouldn't use standalone controllers to bind route to component. This makes things way simpler.
const someState = {
name: 'someState',
url: '/p/{id}',
resolves: {
// key name will be mapped to component bindings dirrectly. This is important!
profile: (someService, $stateParams) => someService.getTheData($stateParams.id);
},
component: 'somePage'
};
export someState;
Then you could test your components without resolvers:
import myComponentModule from './my-component.js';
describe('Component: somePage component', function () {
let $componentController;
beforeEach(module(myComponentModule));
beforeEach(inject(function(_$componentController_) {
$componentController = _$componentController_;
}));
it('should display profile information', function() {
// you can just pass whatever you want,
// it's much more easier than trying to mock something
// we don't even depend on. fully isolated test.
const bindings = {
profile: {
name: 'John Doe'
}
};
const ctrl = $componentController('somePage', null, bindings);
expect(ctrl.profile.name).toBeDefined();
});
});
Since you test your components and services in isolation, you don't need to test uiRouter stuff yourself. uiRouter already well tested library. Instead you could write some e2e tests to check that everything is working fine (some tests only).
Hope this will help.
@albert5287 solution seems to work. Anyway I experience an odd behavior. The resolved variable isn't instantly available on my controller. For testing purpose and to sort out async issue I gave it a try using $timeout in resolve route.
Resolve:
testResolve: $timeout => $timeout(() => ...., 5000)
Controller:
console.log('resolved', this.testResolve)
-> display undefined.
However if I put another $timeout within my Controller and set it to 1millisecond, I get the resolved var.
Controller:
$timeout(() => console.log('resolved from timeout', this.testResolve), 1);
-> displays test data.
I am confirming what @salacis is seeing in my own code.
@Incognito Thats the usual behaviour of an component. Take a look at this.$onInit = () => {}
. Your resolved variables will be there when everything is loaded properly.
Oh, that works much better. I'm now using this pattern:
class SomeController {
constructor(SomeApi, $stateParams) {
"ngInject";
this.SomeApi=SomeApi;
this.someId = +$stateParams.someId;
}
$onInit() {
// Actual logic...
}
}
Thanks for the tip!
That's not the appropriate way to use it. look at this https://toddmotto.com/angular-1-5-lifecycle-hooks
After reading the onInit part of code, the specific problem you see with that code was not obvious to me. Is the issue that it contains all logic, or that it is defined at instantiation instead of in the constructor?
@Incognito $onInit
method is responsible to component initialization. Constructor should not contain any logic.
By doing this you could create instance of component, pass data to it and then call $onInit
method for component initialization. This simplifies testing and extension of components.
I don't quite get it.
In the Todo example this code exists:
export class TodoItemController {
constructor(todoList) {
"ngInject";
this.todoList = todoList;
this.isEditing = false;
}
onDestroyClick() {
this.todoList.remove(this.task);
}
// ...
In my example I also inject dependencies via the controller and perform logic inside other methods. In the Todo example it's a method on the controller, and in mine it is a lifecycle hook.
.... is the issue my injection of $stateParams
in the constructor? I don't see the Todo example as very different from something such as:
export class TodoItemController {
constructor(todoList) {
"ngInject";
this.todoList = todoList;
this.isEditing = false;
}
$onInit() {
this.todoList.remove(this.task);
}
I think I can test both, no?
In the Todo example this code exists:
todoList
is dependency of an component. There is no any logic or data retrieval. As for isEditing
, it is just initial state of this component. Only component itself responsible for it's initial state (encapsulation and stuff).
is the issue my injection of $stateParams in the constructor?
Yep. By doing this you couple your component to specific route which isn't that flexible. Components != screens. You could have some-kind of top-level components which defines screens, but they should delegate all presentation responsibilities to lower-level components and pass data via binding.
It will be better if you pass all resolved values directly to components binding. UI Router allows that.
I think I can test both, no?
Yes, you can, but when state defined via bindings it's easier.
Oh I think I get it now. That would let me modify stateful dependencies per instance instead of component instead of the same dependencies every time via ng inject. That's cool.
Guys, with this syntax:
It's possible to use resolve? Or do we need a controller property in the state in order to use it?
Thank you.