Reputation: 31580
I've got a known list of supported values for parameter A. I need to validate the state parameter's value before any of the state's resolves are triggered, and if the value is invalid, to supply a supported value. My initial thought was to use an injectable function for the parameter's value property:
params: {
A: {
value: [
'$stateParams',
'validator',
function validateParamA($stateParams, validator) {
// return some value
}
}
}
}
However, $stateParams
is unpopulated at this point (I was hoping for a preview version like what you get in a resolve), and also this would probably set a default value, not the value of the $stateParam
itself. So I'm looking for something like urlRouterProvider.when
's $match
.
My next idea was to just use a urlRouterProvider.when
. No-dice: To my dismay, this fires after the state has resolved.
My next idea was to hijack urlMatcherFactory's encode. Same deal (fires after).
Ugh! The problem is that a controller is being executed outside of UI Router via ngController
. Moving it inside should fix the sequence issue (and then when
should work). Will update later today.
Upvotes: 4
Views: 4095
Reputation: 2408
I recently had the same problem and solved it with a $q.defer()
call in the resolve callback. I had some default parameters for a calender view and wanted to validate the parameters before hand. Didn't find anything else on this topic but it seems like a quite solid solution. This is my sample state:
$stateProvider.state(
'tasks.list',
{
url : '/:type?month&year&dueDate', // optional params for filtering
params : {
type : {
value : 'all' // all|open|assigned|my
},
month : {
value : 1, // current month, needs a callback, no static value
type : 'int'
},
year : {
value : 2016, // current year, needs a callback, no static value
type : 'int'
},
dueDate : {
value : undefined, // 'no default value' - parses 2016-04-23 to da js date object
type : 'date'
}
},
resolve : {
validParams : ['$q', '$stateParams',
function($q, $stateParams) {
var deferred = $q.defer();
var allowedTypes = ['all', 'open', 'assigned', 'my'];
if (allowedTypes.indexOf($stateParams.type.trim().toLowerCase()) < 0) {
// deferred.reject(reason) also takes a simple string or nothing, you can use this information on UI.Router's $stateChangeError Event
deferred.reject({
error : 'Invalid Value',
param : 'type',
value : $stateParams.type
});
}
if ($stateParams.month < 1 || $stateParams.month > 12) {
deferred.reject({
error : 'Invalid Value',
param : 'month',
value : $stateParams.month
});
}
if ($stateParams.year < 2014 || $stateParams.year > 2099) {
deferred.reject({
error : 'Invalid Value',
param : 'year',
value : $stateParams.month
});
}
// if a _deferred object was already rejected, it can't be resolved anymore, so this doesn't hurt at all
deferred.resolve('Valid Values');
return _deferred.promise;
}],
taskListModel : ['TaskHttpService', '$stateParams',
function(TaskHttpService, $stateParams) {
// no matter if 'validParams' is resolved or not, this is called - so you might want to validate again or do some other check if you make an ajax call
return TaskHttpService.loadTasks({
month : $stateParams.month,
year : $stateParams.year,
dueDate : $stateParams.dueDate
});
}]
},
views : {
menu : {
templateUrl : '/menu.html',
controller : 'MenuController'
},
body : {
templateUrl : '/body.html',
controller : 'BodyController'
}
}
}
)
As mentioned in an inline comment, you can pass simple strings or entire objects to deferred.reject(reason)
https://docs.angularjs.org/api/ng/service/$q - you might want to listen on the $stateChangeError
on $rootScope
to do anything with the information:
// test for failed routing access, redirect to index page
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
if(__.isObject(error)) {
switch(error.error) {
case 'Access Denied':
$state.go('index');
break;
case 'Invalid Value':
console.warn('Invalid URL Params to State %o %o', toState.name, error);
break;
}
}
});
Update After completing the answer I recognized that you asked for a validation before the resolved parameters are called. In the title you say "before state resolves" - so with a rejected promise the state isn't resolved. Maybe this can help you anyways
Upvotes: 2
Reputation: 31580
A MarcherFactory did the trick. After I corrected that ngController nonsense and brought those controllers inside UI Router, it worked just as I expected.
// url: '/{locale:locale}'
function validateLocale(validator, CONSTANTS, value) {
var match = validator(value);
if (match === true) {
return value;
}
if (match) { // partial match
newLocale = match;
} else {
newLocale = CONSTANTS.defaultLocale;
}
return newLocale;
}
$urlMatcherFactoryProvider.type(
'locale',
{
pattern: ROUTING.localeRegex
},
[
// …
function localeFactory(validator, CONSTANTS) {
return {
encode: validateLocale.bind({}, validator, CONSTANTS)
};
}
]
);
:Rage:
Upvotes: 4
Reputation: 1967
What about splitting the state into two: one that does the validation, one that is the actual target state.
$stateProvider
.state('validationState', {
// The controller below will not get instantiated without defining template
template: '',
controller: function ($stateParams, $state) {
if (/* your validation */) {
$state.go('targetState', /* simply forward the valid parameter */);
} else {
$state.go('targetState', /* provide your valid parameter value */);
}
}
})
.state('targetState', {
// whatever you want to resolve, yadda yadda
});
Not sure the controller can be replaced with onEnter
in the validation state definition, but maybe.
Upvotes: 0
Reputation: 636
If A is resolved in the state resolves and other resolves depend on it, you'll be able to check the $stateParams and provide an alternative value if needed. Other resolves will be resolved after A.
$stateProvider
.state('state', {
resolve: {
A: ['$stateParams', 'validator', function($stateParams, validator) {
return validator.validate($stateParams.A) ? $stateParams.A : 'default';
}],
otherResolve: ['A', function(A) {
///
}
}
});
Other resolves should not use the $stateParams directly, I don't know if it is a problem for you.
Upvotes: 1