5

I want to make a route with has a mandatory parameter. If not, it should fall into

$urlRouterProvider.otherwise("/home");

Current route:

function router($stateProvider) {
        $stateProvider.state("settings", {
            url: "^/settings/{id:int}",
            views: {
                main: {
                    controller: "SettingsController",
                    templateUrl: "settings.html"
                }
            }
        });
    }

Currently both the routes below are valid:

  1. http://myapp/settings //Should be invalid route
  2. http://myapp/settings/123

Any ideas?

iH8
  • 27,722
  • 4
  • 67
  • 76
Varun
  • 307
  • 3
  • 16
  • IMO, there shouldn't be a problem with accessing the `/settings` path, since it doesn't correspond to any state. The actual issue should happen when accessing the `/settings/` path, because it will assign the empty string (`""`) to the `id` parameter. – Evgeniya Manolova Oct 02 '17 at 18:17

5 Answers5

0

Use a state change start listener to check if params were passed:

  $rootScope.$on('$stateChangeStart',
                function (event, toState, toParams, fromState, fromParams) {
      if(toState.name==="settings")
      {
          event.preventDefault(); //stop state change
          if (toParams.id===undefined)
           $state.go("home");
          else
           $state.go(toState, toParams);

      }
  });
Frozen Crayon
  • 5,172
  • 8
  • 36
  • 71
  • I have several destinations which require mandatory parameter. It would become an extremely large if/else block with the example above. Is there some generic way to deal with this? – Varun Mar 18 '15 at 19:11
0

The following solution is valid for ui-router 1.0.0:

.config(($stateProvider, $transitionsProvider) => {

  //Define state
  $stateProvider.state('verifyEmail', {
    parent: 'portal',
    url: '/email/verify/:token/:optional',
    component: 'verifyEmail',
    params: {
      token: {
        type: 'string',
      },
      optional: {
        value: null,
        squash: true,
      },
    },
  });

  //Transition hooks
  $transitionsProvider.onBefore({
    to: 'verifyEmail',
  }, transition => {

    //Get params
    const params = transition.params();

    //Must have token param
    if (!params.token) {
      return transition.router.stateService.target('error', {
        type: 'page-not-found',
      });
    }
  });
 })

The above will make the :token parameter mandatory and the :optional parameter optional. If you try to browse to the page without the token parameter it will fail the transition and redirect to your error page. If you omit the :optional parameter however, it will use the default value (null).

Remember to use squash: true on the trailing optional parameters, because otherwise you'll also get a 404 if you omit the trailing / in the URL.

Note: the hook is required, because if you browse to email/verify/ with a trailing slash, ui-router will think the token parameter is an empty string. So you need the additional handling in the transition hook to capture those cases.

Adam Reis
  • 4,165
  • 1
  • 44
  • 35
0

In my app I had to make required parameters for a lot of routes. So I needed a reusable and DRY way to do it.

I define a constants area in my app to access global code. I use for other things as well.

I run this notFoundHandler at app config time. This is setting up a router state for handling errors. It is setting the otherwise route to this error route. You could define a different route for when a required parameter is missing, but for us this was defined as being the same as a 404 experience.

Now at app run time I also define a stateChangeErrorHandler which will look for a rejected route resolve with the 'required-param' string.

angular.module('app')
    .constant('constants', constants)
    .config(notFoundHandler)
    .run(stateChangeErrorHandler);

// use for a route resolve when a param is required
function requiredParam(paramName) {
    return ['$stateParams', '$q', function($stateParams, $q) {
        // note this is just a truthy check.  if you have a required param that could be 0 or false then additional logic would be necessary here
        if (!$stateParams[paramName]) {
            // $q.reject will trigger the $stateChangeError
            return $q.reject('required-param');
        }
    }];
}
    
var constants = {
    requiredParam: requiredParam,
    // define other constants or globals here that are used by your app
};

// define an error state, and redirect to it if no other route matches
notFoundHandler.$inject = ['$stateProvider', '$urlRouterProvider'];
function notFoundHandler($stateProvider, $urlRouterProvider) {
    $stateProvider
        //abstract state so that we can hold all our ingredient stuff here
        .state('404', {
            url: '/page-not-found',
            views: {
                '': {
                    templateUrl: "/app/error/error.tpl.html",
                }
            },
            resolve: {
                $title: function () { return 'Page Not Found'; }
            }
        });

    // redirect to 404 if no route found
    $urlRouterProvider.otherwise('/page-not-found');
}

// if an error happens in changing state go to the 404 page
stateChangeErrorHandler.$inject = ['$rootScope', '$state'];
function stateChangeErrorHandler($rootScope, $state) {
    $rootScope.$on('$stateChangeError', function(evt, toState, toParams, fromState, fromParams, error) {
        if (error && error === 'required-param') {
// need location: 'replace' here or back button won't work on error page
            $state.go('404', null, {
                location: 'replace'
            });
        }
    });
}

Now, elsewhere in the app, when I have a route defined, I can make it have a required parameter with this route resolve:

angular.module('app')
    .config(routeConfig);

routeConfig.$inject = ['$stateProvider', 'constants'];

function routeConfig($stateProvider, constants) {
    $stateProvider.state('app.myobject.edit', {
     url: "/:id/edit",
     views: {
      '': {
          template: 'sometemplate.html',
          controller: 'SomeController',
          controllerAs: 'vm',
      }
     },
     resolve: {
      $title: function() { return 'Edit MyObject'; },
// this makes the id param required
      requiredParam: constants.requiredParam('id')
     }
 });
}
FirstVertex
  • 3,657
  • 34
  • 33
0

I'd like to point out that there shouldn't be any problem with accessing the /settings path, since it doesn't correspond to any state, unless you've used inherited states (see below).

The actual issue should happen when accessing the /settings/ path, because it will assign the empty string ("") to the id parameter.

If you didn't use inherited states

Here's a solution in plunker for the following problem:

accessing the /state_name/ path, when there's a state with url /state_name/:id

Solution explanation

It works through the onBefore hook (UI router 1.x or above) of the Transition service, which prevents transitioning to states with missing required parameters.

In order to declare which parameters are required for a state, I use the data hash like this:

.state('settings', {
  url: '/settings/:id',
  data: {
    requiredParams: ['id']
  }
});

Then in app.run I add the onBefore hook:

transitionService.onBefore({}, function(transition) {
  var toState = transition.to();
  var params = transition.params();
  var requiredParams = (toState.data||{}).requiredParams || [];
  var $state = transition.router.stateService;

  var missingParams = requiredParams.filter(function(paramName) {
    return !params[paramName];
  });

  if (missingParams.length) {
    /* returning a target state from a hook 
       issues a transition redirect to that state */
    return $state.target("home", {alert: "Missing params: " + missingParams});
  }
});

If you used inherited states

You could implement the same logic via inherited states:

function router($stateProvider) {
  $stateProvider
    .state('settings', {
      url: '/settings'
    })
    .state('settings.show", {
      url: '/:id'
    });
}

then you'd need to add the abstract property to the parent declaration, in order to make /settings path inaccessible.

Solution explanation

Here's what the documentation says about the abstract states:

An abstract state can never be directly activated. Use an abstract state to provide inherited properties (url, resolve, data, etc) to children states.

The solution:

function router($stateProvider) {
  $stateProvider
    .state('settings', {
      url: '/settings',
      abstract: true
    })
    .state('settings.show", {
      url: '/:id'
    });
}

Note: that this only solves the issue with /settings path and you still need to use the onBefore hook solution in order to also limit the access to /settings/.

Evgeniya Manolova
  • 2,542
  • 27
  • 21
-1

it is not very well documented, but you can have required and optional parameters, and also parameters with default values.

Here is how you can set required params:

function router($stateProvider) {
  $stateProvider.state("settings", {
    url: "^/settings/{id:int}",
    params: {
      id: {}
    },
    views: {
      main: {
        controller: "SettingsController",
        templateUrl: "settings.html"
      }
    }
  });
}

I never used params with curly brackets, just with the semicolon, like this url: "^/settings/:id", but from what I read, those are equivalent.

For other types of parameters, please see the other half of my answer here: AngularJS UI Router - change url without reloading state

Please note that when I added that answer, I had to build ui-router from source, but I read that functionality has been added to the official release by now.

Community
  • 1
  • 1
wiherek
  • 1,923
  • 19
  • 25
  • So what makes the parameter mandatory? - adding `^` to the `url` template or defining a `params` dictionary with keys which values are object literals? – kidroca Oct 29 '16 at 18:55
  • based on my testing - neither do. also {} doesn't seem to be allowed as a default value for a param – Sam Mar 15 '17 at 04:26
  • this is quite invalid answer. The _params_ property only lets you declare params that are _not in_ the url, or set default values for _missing_ params. First it's wrong to set a default value for the `id` param, and second - it's even more wrong to default an `int` parameter to an `object` value – Evgeniya Manolova Oct 02 '17 at 18:29