NG6-starter icon indicating copy to clipboard operation
NG6-starter copied to clipboard

How to use resolve

Open santios opened this issue 10 years ago • 48 comments

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.

santios avatar Jun 17 '15 23:06 santios

what do you mean by resolve? are you talking about

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

PatrickJS avatar Jun 18 '15 00:06 PatrickJS

@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 avatar Jun 18 '15 00:06 santios

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

PatrickJS avatar Jun 18 '15 00:06 PatrickJS

@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 avatar Jun 18 '15 00:06 santios

$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

PatrickJS avatar Jun 18 '15 01:06 PatrickJS

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

martinmicunda avatar Jun 25 '15 08:06 martinmicunda

@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

PatrickJS avatar Jun 25 '15 16:06 PatrickJS

@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 avatar Jun 25 '15 16:06 martinmicunda

@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 avatar Jun 25 '15 16:06 santios

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 avatar Aug 27 '15 22:08 eshcharc

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

santios avatar Aug 27 '15 22:08 santios

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

eshcharc avatar Aug 27 '15 22:08 eshcharc

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

PatrickJS avatar Sep 21 '15 20:09 PatrickJS

+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 avatar Oct 09 '15 15:10 gad2103

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 avatar Jan 04 '16 14:01 uriklar

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 avatar Jan 04 '16 14:01 uriklar

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 avatar Jan 04 '16 15:01 eshcharc

In due time :-) thanks!

uriklar avatar Jan 04 '16 15:01 uriklar

@eshcharc can you provide an example?

fesor avatar Jan 04 '16 15:01 fesor

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 avatar Jan 04 '16 15:01 eshcharc

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

fesor avatar Jan 04 '16 16:01 fesor

State and data manipulation is best achieved with Scan operator.

eshcharc avatar Jan 04 '16 16:01 eshcharc

@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 avatar Jan 16 '16 22:01 aneurysmjs

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

julius-retzer avatar Feb 08 '16 23:02 julius-retzer

@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 avatar Feb 12 '16 00:02 aneurysmjs

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 avatar Mar 20 '16 13:03 ranbuch

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 avatar Mar 20 '16 13:03 ranbuch

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?

fesor avatar Mar 20 '16 14:03 fesor

That solution looks neat! Thank you.

ranbuch avatar Mar 20 '16 18:03 ranbuch

@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 avatar Mar 23 '16 18:03 pibouu