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

How to use resolve #12

Open
santios opened this Issue Jun 17, 2015 · 48 comments

Comments

Projects
None yet
@santios

santios commented Jun 17, 2015

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.

@gdi2290

This comment has been minimized.

Show comment
Hide comment
@gdi2290

gdi2290 Jun 18, 2015

Owner

what do you mean by resolve? are you talking about

$stateProvider
        .state('home', {
            url: '/',
            template: '<home></home>',
            resolve: {
              yourData: function(yourService) {
                return yourService.getAsyncPromise()
              }
            }
        });
Owner

gdi2290 commented Jun 18, 2015

what do you mean by resolve? are you talking about

$stateProvider
        .state('home', {
            url: '/',
            template: '<home></home>',
            resolve: {
              yourData: function(yourService) {
                return yourService.getAsyncPromise()
              }
            }
        });
@santios

This comment has been minimized.

Show comment
Hide comment
@santios

santios Jun 18, 2015

@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 commented Jun 18, 2015

@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.

@gdi2290

This comment has been minimized.

Show comment
Hide comment
@gdi2290

gdi2290 Jun 18, 2015

Owner

@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;
Owner

gdi2290 commented Jun 18, 2015

@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;
@santios

This comment has been minimized.

Show comment
Hide comment
@santios

santios Jun 18, 2015

@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?

santios commented Jun 18, 2015

@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?

@gdi2290

This comment has been minimized.

Show comment
Hide comment
@gdi2290

gdi2290 Jun 18, 2015

Owner
$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

Owner

gdi2290 commented Jun 18, 2015

$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

@martinmicunda

This comment has been minimized.

Show comment
Hide comment
@martinmicunda

martinmicunda Jun 25, 2015

@gdi2290 I have exactly same issue with resolve as @santios so what is the proper solution for this problem?

martinmicunda commented Jun 25, 2015

@gdi2290 I have exactly same issue with resolve as @santios so what is the proper solution for this problem?

@gdi2290

This comment has been minimized.

Show comment
Hide comment
@gdi2290

gdi2290 Jun 25, 2015

Owner

@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

Owner

gdi2290 commented Jun 25, 2015

@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

@martinmicunda

This comment has been minimized.

Show comment
Hide comment
@martinmicunda

martinmicunda Jun 25, 2015

@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 commented Jun 25, 2015

@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');
                    });
            }
        });
@santios

This comment has been minimized.

Show comment
Hide comment
@santios

santios Jun 25, 2015

@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.

santios commented Jun 25, 2015

@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.

@eshcharc

This comment has been minimized.

Show comment
Hide comment
@eshcharc

eshcharc Aug 27, 2015

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 commented Aug 27, 2015

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.

@santios

This comment has been minimized.

Show comment
Hide comment
@santios

santios Aug 27, 2015

@eshcharc That's clever, thank you for sharing.

santios commented Aug 27, 2015

@eshcharc That's clever, thank you for sharing.

@eshcharc

This comment has been minimized.

Show comment
Hide comment
@eshcharc

eshcharc Aug 27, 2015

@santios If you have a spare time, please open a PR.

eshcharc commented Aug 27, 2015

@santios If you have a spare time, please open a PR.

@gdi2290

This comment has been minimized.

Show comment
Hide comment
@gdi2290

gdi2290 Sep 21, 2015

Owner

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

Owner

gdi2290 commented Sep 21, 2015

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

@gad2103

This comment has been minimized.

Show comment
Hide comment
@gad2103

gad2103 Oct 9, 2015

+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.

gad2103 commented Oct 9, 2015

+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.

@uriklar

This comment has been minimized.

Show comment
Hide comment
@uriklar

uriklar Jan 4, 2016

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?

uriklar commented Jan 4, 2016

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?

@uriklar

This comment has been minimized.

Show comment
Hide comment
@uriklar

uriklar Jan 4, 2016

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: '='
  }
};

uriklar commented Jan 4, 2016

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: '='
  }
};
@eshcharc

This comment has been minimized.

Show comment
Hide comment
@eshcharc

eshcharc Jan 4, 2016

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...

eshcharc commented Jan 4, 2016

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...

@uriklar

This comment has been minimized.

Show comment
Hide comment
@uriklar

uriklar Jan 4, 2016

In due time :-) thanks!

uriklar commented Jan 4, 2016

In due time :-) thanks!

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Jan 4, 2016

Collaborator

@eshcharc can you provide an example?

Collaborator

fesor commented Jan 4, 2016

@eshcharc can you provide an example?

@eshcharc

This comment has been minimized.

Show comment
Hide comment
@eshcharc

eshcharc Jan 4, 2016

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 commented Jan 4, 2016

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.

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Jan 4, 2016

Collaborator

@eshcharc I know about reactive programming, I only doesn't thought about representing state as data stream (in angular components context)

Collaborator

fesor commented Jan 4, 2016

@eshcharc I know about reactive programming, I only doesn't thought about representing state as data stream (in angular components context)

@eshcharc

This comment has been minimized.

Show comment
Hide comment
@eshcharc

eshcharc Jan 4, 2016

State and data manipulation is best achieved with Scan operator.

eshcharc commented Jan 4, 2016

State and data manipulation is best achieved with Scan operator.

@aneurysmjs

This comment has been minimized.

Show comment
Hide comment
@aneurysmjs

aneurysmjs Jan 16, 2016

@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

aneurysmjs commented Jan 16, 2016

@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

@wormyy

This comment has been minimized.

Show comment
Hide comment
@wormyy

wormyy Feb 8, 2016

Contributor

@blackendstudios but that is ngRoute, this project uses ui-router

Contributor

wormyy commented Feb 8, 2016

@blackendstudios but that is ngRoute, this project uses ui-router

@aneurysmjs

This comment has been minimized.

Show comment
Hide comment
@aneurysmjs

aneurysmjs Feb 12, 2016

@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"

aneurysmjs commented Feb 12, 2016

@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"

@ranbuch

This comment has been minimized.

Show comment
Hide comment
@ranbuch

ranbuch Mar 20, 2016

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.

ranbuch commented Mar 20, 2016

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.

@ranbuch

This comment has been minimized.

Show comment
Hide comment
@ranbuch

ranbuch Mar 20, 2016

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.

ranbuch commented Mar 20, 2016

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.

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Mar 20, 2016

Collaborator

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?

Collaborator

fesor commented Mar 20, 2016

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?

@ranbuch

This comment has been minimized.

Show comment
Hide comment
@ranbuch

ranbuch Mar 20, 2016

That solution looks neat!
Thank you.

ranbuch commented Mar 20, 2016

That solution looks neat!
Thank you.

@pibouu

This comment has been minimized.

Show comment
Hide comment
@pibouu

pibouu Mar 23, 2016

@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 commented Mar 23, 2016

@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 ?

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Mar 23, 2016

Collaborator

@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).

Collaborator

fesor commented Mar 23, 2016

@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).

@aneurysmjs

This comment has been minimized.

Show comment
Hide comment
@aneurysmjs

aneurysmjs Mar 24, 2016

@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>`
}

aneurysmjs commented Mar 24, 2016

@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>`
}
@pibouu

This comment has been minimized.

Show comment
Hide comment
@pibouu

pibouu Mar 24, 2016

@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.

pibouu commented Mar 24, 2016

@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.

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Mar 24, 2016

Collaborator

@blackendstudios I use latest version of uiRouter, so i just don't need controller at all (angular-ui/ui-router@0f6aea6). But yes, you are right.

Collaborator

fesor commented Mar 24, 2016

@blackendstudios I use latest version of uiRouter, so i just don't need controller at all (angular-ui/ui-router@0f6aea6). But yes, you are right.

@Blaze34

This comment has been minimized.

Show comment
Hide comment
@Blaze34

Blaze34 Apr 29, 2016

@fesor 0.2.19-dev not exist. 1.0.0-alpha.4 at that moment

Blaze34 commented Apr 29, 2016

@fesor 0.2.19-dev not exist. 1.0.0-alpha.4 at that moment

@albert5287

This comment has been minimized.

Show comment
Hide comment
@albert5287

albert5287 Jun 17, 2016

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.

albert5287 commented Jun 17, 2016

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.

@ttbarnes

This comment has been minimized.

Show comment
Hide comment
@ttbarnes

ttbarnes Dec 9, 2016

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 commented Dec 9, 2016

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?

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Dec 9, 2016

Collaborator

@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.

Collaborator

fesor commented Dec 9, 2016

@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.

pedrosobral pushed a commit to pedrosobral/NG6-starter that referenced this issue Jan 12, 2017

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Apr 8, 2017

@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.

ghost commented Apr 8, 2017

@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.

@Incognito

This comment has been minimized.

Show comment
Hide comment
@Incognito

Incognito Apr 13, 2017

I am confirming what @salacis is seeing in my own code.

Incognito commented Apr 13, 2017

I am confirming what @salacis is seeing in my own code.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Apr 13, 2017

@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.

ghost commented Apr 13, 2017

@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.

@Incognito

This comment has been minimized.

Show comment
Hide comment
@Incognito

Incognito Apr 13, 2017

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!

Incognito commented Apr 13, 2017

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!

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Apr 13, 2017

That's not the appropriate way to use it. look at this https://toddmotto.com/angular-1-5-lifecycle-hooks

ghost commented Apr 13, 2017

That's not the appropriate way to use it. look at this https://toddmotto.com/angular-1-5-lifecycle-hooks

@Incognito

This comment has been minimized.

Show comment
Hide comment
@Incognito

Incognito Apr 13, 2017

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 commented Apr 13, 2017

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?

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Apr 15, 2017

Collaborator

@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.

Collaborator

fesor commented Apr 15, 2017

@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.

@Incognito

This comment has been minimized.

Show comment
Hide comment
@Incognito

Incognito Apr 16, 2017

I don't quite get it.

In the Todo example this code exists:

https://github.com/AngularClass/NG6-todomvc-starter/blob/master/src/app/components/todoItem.component.js

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?

Incognito commented Apr 16, 2017

I don't quite get it.

In the Todo example this code exists:

https://github.com/AngularClass/NG6-todomvc-starter/blob/master/src/app/components/todoItem.component.js

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?

@fesor

This comment has been minimized.

Show comment
Hide comment
@fesor

fesor Apr 16, 2017

Collaborator

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.

Collaborator

fesor commented Apr 16, 2017

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.

@Incognito

This comment has been minimized.

Show comment
Hide comment
@Incognito

Incognito Apr 17, 2017

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.

Incognito commented Apr 17, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment