0

I've run into quite a novel problem with ui router. I'm basically trying to query my API for a template url, which is stored and returned as a Resource object. The problem, is it seems that the templateUrl function is returning as undefined. My code is as follows:

(function() {
    'use strict';

  angular.module('myApp')
    .config(function ($stateProvider, PageProvider, $httpProvider) {
      $stateProvider
        .state('core', {
          abstract: true,
          templateUrl: 'app/core/core.index.html',
          controller: 'CoreController',
          controllerAs: 'vm'
        })
        /* Parent page view */
        .state('core.page', {
          url: '/:slug',
          views: {
            'page_content@core': {
              templateUrl: function(params) {
                var slug = params.slug;
                // only need to load core template
                var url = 'assets/templates/' + slug + '.html';

                return url;
              },
              controller: 'PageController',
              controllerAs: 'vm'
            }
          }
        })
        .state('core.page.child', {
          url: '/:child',
          views: {
            'page_content@core': {
              templateUrl: function($stateParams) {
                PageProvider
                  .$get() // $get() returns Page service
                  .findOne($stateParams.child)
                  .$promise
                  .then(function(data){
                    return data.template.url;
                  });
              }
            }
          }
        });
      });
})();

I set a breakpoint for my state, 'core.page.child', to see what variables were available in the scope, and I found something quite strange:

url here is undefined...

but it logs out the correct templateUrl value from the promise

I really don't understand why this is happening, since .then(cb) is only called AFTER the promise is resolved, which means it should return the correct value as expected - but it doesn't.

Any help would be much appreciated.

Edit: I should add that what is happening is that ui-router is simply not loading my template at all - I get an empty ui-view. I had initially thought it might be a problem with my $stateProvider configuration, but that doesn't seem to be the case.

Edit2: I dug a little into the source code based on the call stack. It seems that both of you are correct - ui-router does NOT work with promises.

/**
 * @ngdoc function
 * @name ui.router.util.$templateFactory#fromConfig
 * @methodOf ui.router.util.$templateFactory
 *
 * @description
 * Creates a template from a configuration object. 
 *
 * @param {object} config Configuration object for which to load a template. 
 * The following properties are search in the specified order, and the first one 
 * that is defined is used to create the template:
 *
 * @param {string|object} config.template html string template or function to 
 * load via {@link ui.router.util.$templateFactory#fromString fromString}.
 * @param {string|object} config.templateUrl url to load or a function returning 
 * the url to load via {@link ui.router.util.$templateFactory#fromUrl fromUrl}.
 * @param {Function} config.templateProvider function to invoke via 
 * {@link ui.router.util.$templateFactory#fromProvider fromProvider}.
 * @param {object} params  Parameters to pass to the template function.
 * @param {object} locals Locals to pass to `invoke` if the template is loaded 
 * via a `templateProvider`. Defaults to `{ params: params }`.
 *
 * @return {string|object}  The template html as a string, or a promise for 
 * that string,or `null` if no template is configured.
 */
this.fromConfig = function (config, params, locals) {
  return (
    isDefined(config.template) ? this.fromString(config.template, params) :
    isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
    isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) :
    null
  );
};

It seems that I'll either have to use $stateProvider.decorator, or hack ui-router's core. I think I might just do the latter and submit a PR. I'll be posting this issue on the github repo as well, to see if anyone on the ui-router team has a solution for this problem.

natnai
  • 554
  • 2
  • 10
  • 1
    I don't know if `templateUrl` can operate on _promises_, but if it does, I think you'd at least need to return this _promise_. Did you try to add `return` before `PageProvider.$get()...`? – Tomek Sułkowski Jun 21 '15 at 12:32
  • @TomekSułkowski is right, you cannot expect a templateUrl function to know it's working with a promise, unless you actually return a promise. A 'side effect only' function will not be waited upon to complete. – SirDemon Jun 21 '15 at 12:36
  • Your question is pretty similar to this [SO question](http://stackoverflow.com/questions/25289135/angular-ui-router-decide-child-state-template-on-the-basis-of-parent-resolved-o). Maybe this helps they're using `templateProvider`. – AWolf Jun 21 '15 at 13:30
  • I've posted an issue on the ui-router repo. Will get back to both of you when a solution is found. – natnai Jun 21 '15 at 13:31
  • @AWolf that is certainly worth a try. I was initially put off by 'must return HTML'; I suppose I could make the server statically serve the html file directly with `$http.get`. – natnai Jun 21 '15 at 13:34
  • @AWolf that is certainly worth a try. I was initially put off by 'must return HTML'; I suppose I could make the server statically serve the html file directly with `$http.get`. Also, there is the problem of determining _which_ template file to request. As you can see the route has only one param, /:slug, which means $stateParams will only the slug. I can use the slug to ask the API for the full object, from which I can then access the template's url. One way, I suppose, is to cache everything in a constant with a simple key-value hash, and then get it from there with `templateProvider`. – natnai Jun 21 '15 at 13:41
  • [link](http://stackoverflow.com/questions/26868796/angular-and-ui-router-how-to-set-a-dynamic-templateurl) It seems this is the answer. @AWolf was right. – natnai Jun 21 '15 at 13:50

1 Answers1

3

Working plunk

you can use a resolve and templateProvider to do this.

  .state('child', {
      url: '/:child',
      resolve: {
        childTemplate: function ($stateParams, $templateRequest) {
          return $templateRequest($stateParams.child + ".html");
        }
      },
      templateProvider: function(childTemplate) { 
        return childTemplate;
      }
  });

This creates a resolve which uses the $stateParams to fetch the template (I'm using the $templateRequest to fetch, add to $templateCache, and return the contents all in one). Then, the resolved template content is injected into the view's templateProvider.

Chris T
  • 8,186
  • 2
  • 29
  • 39
  • Thank Chris, this appears to be the most elegant solution! – natnai Jun 23 '15 at 01:35
  • I didn't know that values from the `resolve` block are injected into `templateProvider` functions. I would also like to ask if `templateProvider` only executes when all promises in the `resolve` block are resolved? – natnai Jun 23 '15 at 01:37
  • I believe template provider is invoked after the resolves. Also, it did not receive resolves until recently. See https://github.com/angular-ui/ui-router/issues/330 – Chris T Jun 23 '15 at 02:42
  • Thanks Chris, I'll be closing my issue on the repo. That was very helpful. – natnai Jun 23 '15 at 06:12
  • this doesn't work if i put timeout service in my childTemplate resolve and return some html string after 3 seconds. In this case my templateProvider getting fired first and then my resolve which is not what I expect – Atul Chaudhary May 19 '17 at 01:55