UI Router: How to validate parameter before state resolves - angularjs

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

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:

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

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.

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.

Related

UI-router stateParams cloning objects?

I have this route:
{
name : 'myPage',
url : '/myPage',
views: {
'#': {
component: components.MyComponent.name
}
},
params: {
turtle: {y: 2}
},
resolve : {
turtle: function($stateParams) {
window.turtle = $stateParams.turtle;
window.daaa = $stateParams;
return $stateParams.turtle;
}
}
}
I have this component binding def:
bindings: {
turtle: '<'
}
and the constructor of MyComponent's controller:
class MyComponentController {
let self = this;
this.$onInit = function() {
console.log("-----")
console.log("window.turtle: " + JSON.stringify(window.turtle))
console.log("self.turtle: " + JSON.stringify(self.turtle))
console.log("window.turtle == self.turtle: " + (window.turtle == self.turtle))
console.log("window.turtle == self.$stateParams.turtle: " + (window.turtle == self.$stateParams.turtle))
console.log("window.daaa == self.$stateParams: " + (window.daaa == self.$stateParams))
console.log("-----")
}
}
It prints:
-----
window.turtle: {y:2}
self.turtle: {y:2}
window.turtle == self.turtle: true
window.turtle == self.$stateParams.turtle: false
window.daaa == self.$stateParams: false
-----
This is really strange to me..., made me think that once you're inside a state, UI-router makes a clone of $stateParams (so the $stateParams you see inside the resolves != $stateParams you see in the controller)... and it also makes a clone of each declared params (deep clone???).
I wasn't expecting that. Is it a bug? or a feature (maybe protection mechanism?). Who is the culpable here? UI-router? Or is it angular 1.5 component (with its isolated scope stuffs)?
Thanks in advance for helping me clarify this.
According to ui-router source code (was linked by Daniel here) $stateParams is deprecated and it's advised to use $transition$ injectable instead (see ui-router docs on $transition$)
Here's an example of using it in a resolve function:
$stateProvider.state('a-route', {
// ...
// define params for route
params: {
data: null
},
// set up data to resolve to params.data
resolve: {
data: ($transition$) => {
return $transition$.params().data;
}
}
})
Please, see working example here:
http://jsbin.com/velekot/edit?js,output
NOTE: Docs also claim that one can inject the $transition$ to a controller, but I couldn't make it work, so used a resolve fn.

How to set default value for state params using a service?

I have a state with a param like,
.state('statename',{
url : 'emp/:empId',
...
...
}
This will show the employee details for the given employee id. I want to show the logged in user's details if empId is not provided. I have the logged in user id is a service 'User' and i need to inject this 'User' service and set the value to empId as default value.
The document says that i can set default value for params as follows,
.state('statename',{
url : 'emp/:empId',
params : {
empId : 1
},
...
}
I want to inject the service and provide value dynamically and i tried the following options,
1
...
params : function(User){
return { empId : User.getLoggedInUserId() }
}
...
2
...
params : {
empId : function(User){
return User.getLoggedInUserId();
}
}
...
But nothing worked. I hope those are wrong ways to do it. Is there any way through which i can achieve this functionality?
Use resolve. At resolve function, you can inject in any service that is needed, just like in your controller. This is also the cleanest way to do it - since no global variables are involved, and you still can get your $stateParams work as intended.
.state('statename',{
url : 'emp/:empId',
resolve:{
resolve: {
resolveEmpId: ['$stateParams', 'User', function($stateParams, User) {
if ($stateParams.empId === "" ||$stateParams.empId === null ) {
$stateParams.empId = User.getLoggedInUserId();
}
return $stateParams.empId;
}]
}
...
}
Here is a plnkr to show how it works. Open it in full to see the $stateParams changing.
state('statename', {
url: 'emp/:empId',
templateUrl: 'new.html',
controller: function($scope, $stateParams, User) {
$stateParams.empId : User.getLoggedInUserId()
}
})
This may help you
Handle the state change event and set the param there:
yourApp.run(function($rootScope, User) {
$rootScope.$on('$stateChangeStart', function(e, toState, toParams) {
if (!toParams.empId)
toParams.empId = User.getLoggedInUserId();
});
});

Angular JS - Update Scope value on click

Right now I am passing my parameter through the state like:
.state('app.listing', {
url: '/ad/listing/:adId/{type}',
params: {
adId: {
value: "adId",
squash: false
}, type: {
value: null,
squash: true
}
},
This works as I can get "type" from $stateParams and update my get request.
Is there not a way to do this from a click event and not use $stateParams for passing the "type" param?
I basically have a button that filters results and passes the type param from the button. It would be a lot easier if I can just attach a click event to it which then updates my get request.
Just messing around I tried doing something like
$scope.filter = function(type) {
if(type) {
return type;
}
return '' ;
}
$scope.type = $scope.filter();
Service is like
$http.get(API_ENDPOINT.url + '/listing/' + adId, {
params: {
page: page,
type: type // essentially $scope.type
},
}).
and then on my button I have
<button ng-click="filter('2')"></button>
^ This will pass 2 for type, but won't reinit the http get call on click. Do I need to broadcast the change is there a simple way to do this?
Does this even make sense? The code above is just mock to give an idea, but open to suggestions if any.
Angular never requires you to make broadcasts to reflect changes made to scopevariables via the controller
var typeWatcher = '1';
$scope.filter = function(type){
if (type !== typeWatch)
{
$http.get(API_ENDPOINT.url + '/listing/' + adId, {
params: {
page: page,
type: type // essentially $scope.type
},
});
typeWatcher = type;
}
};
You can wrap your get call in a function & call it after the filter function in ng-click
$scope.functionName = function () {
return $http.get(API_ENDPOINT.url + '/listing/' + adId, {
params: {
page: page,
type: type // essentially $scope.type
}
})
}
then in HTML
<button ng-click="filter('2'); functionName()"></button>
Well, To call $http.get method on click,
$scope.filter = function(type) {
if(type) {
//call the method using service.methodName
return type;
}
return '' ;
}
and wrap that $http.get method to one function.
Hope it helps you.
Cheers

Angular ui router view loaded but not passing parameters

I'm working on a website with angular ui-router. There is a page which needs to pass some parameters to another view. I defined my states like this:
.state('locaties', {
url: "/locaties",
data: {rule: function($cookieStore) {} },
controller: "FranchisesCtrl",
templateUrl: "view/locaties.html"
})
.state('locaties.detail', {
params: {
locatieID: 1,
locatieName: "Derptown",
locatieLat: 50,
locatieLong: 50
},
url: "/:locatieName",
controller: "LocatieDetailCtrl",
templateUrl: "view/locatie.html",
resolve: {
locatiedetail:
function ($stateParams, $http){
var url ="http://website/api/franchises/" + $stateParams.locatieID + "/nl.json";
return $http.get(url).then(function(res){
return res.data;
});
}
}
})
Inside LocatieDetailCtrl there's this
angular.module('PremiumMeat').controller('FranchisesDetailCtrl',
function ($scope, $window, franchisedetail) {
$scope.franchiseDetail = franchisedetail;
});
The "Locaties" (plural) view works properly and when I click on a specific "locatie" (single), the url changes and the view gets loaded within the locaties view and no parameters are passed. On the image you can see the top 2 items from the "locaties" view. Then a single locatie is loaded under the "locaties" view. This should be a new page (view) with the parameters from the clicked locatie. Can anyone help me / explain, I'm rather new to angular, thank you.
Solution
The parameters where hard-coded, to make them dynamic, syntax needed adjustment according to angular docs.
params: {
locatieID: {value : "1"},
locatieName: {value : "Stad"},
locatieDescr: {value : "Beschrijving"},
locatieLat: {value: 51.2},
locatieLong: {value : 4.4}
},
Where parameters are passed with ui-href like this
<a ui-sref="locaties.detail({
locatieID: item.id,
locatieName: item.name,
locatieDescr: item.description,
locatieLat: item.location[0].lat,
locatieLong: item.location[0].long
})"
class="detail">Bekijk detail >></a>
The 'params' defined should return the key-value pair object.
But it is a better practice if you are passing values from one state to another to use 'data' instead of appending everything in the URL.
The following code should work :
//The following values are default values of the parameters
.state('locaties.detail', {
params: {
locatieID: '1',
locatieName: 'Derptown',
locatieLat: '50',
locatieLong: '50'
}, ........
This should work. The values expected are of string type and not number.
As far as your LocatieDetailCtrl is concerned, you need to inject what you have in the resolve of the 'locaties.detail' state (i.e. 'locatiedetail'). So your 'LocatieDetailCtrl' should look like following:
angular.module('PremiumMeat').controller('FranchisesDetailCtrl',
function ($scope, $window, franchisedetail, locatiedetail) {
$scope.franchiseDetail = franchisedetail; //make sure you have franchiseDetail as well.
$scope.locatiedetail = locatiedetail;
});
I hope that will work.

AngularJS - change $location silently - remove query string

Is there any way to silently change the route in the url bar using angular?
The user clicks a link for the email that goes to:
/verificationExecuted?verificationCode=xxxxxx
When the page loads I want to read the verificationCode and then clear it:
if($location.path() == '/verificationExecuted'){
this.registrationCode = this.$location.search()["verificationCode"];
this.$location.search("verificationCode", null); //Uncomment but make this silent!
if(registrationCode != null) {
....
}
else $location.path("/404");
}
What happens when I clear it is the remaining part of the route ("/verificationExecuted") remains buts the route re-triggers so it comes around again with no verificationCode and goes straight to 404.
I want to remove the code without doing anything else.
You can always set the reloadOnSearch option on your route to be false.
It will prevent the route from reloading if only the query string changes:
$routeProvider.when("/path/to/my/route",{
controller: 'MyController',
templateUrl: '/path/to/template.html',
//Secret Sauce
reloadOnSearch: false
});
try this
$location.url($location.path())
See documentation for more details about $location
I had a similar requirement for one of my projects.
What I did in such a case was make use of a service.
app.factory('queryData', function () {
var data;
return {
get: function () {
return data;
},
set: function (newData) {
data = newData
}
};
});
This service was then used in my controller as:
app.controller('TestCtrl', ['$scope', '$location', 'queryData',
function ($scope, $location, queryData) {
var queryParam = $location.search()['myParam'];
if (queryParam) {
//Store it
queryData.set(queryParam);
//Reload same page without query argument
$location.path('/same/path/without/argument');
} else {
//Use the service
queryParam = queryData.get();
if (queryParam) {
//Reset it so that the next cycle works correctly
queryData.set();
}
else {
//404 - nobody seems to have the query
$location.path('/404');
}
}
}
]);
I solved this by adding a method that changes the path and canceling the event.
public updateSearch(){
var un = this.$rootScope.$on('$routeChangeStart', (e)=> {
e.preventDefault();
un();
});
this.$location.search('new',search.searchFilter);
if (!keep_previous_path_in_history) this.$location.replace();
}

Resources