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

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();
});
});

Related

ui-router 1.x.x change $transition$.params() during resolve

Trying to migrate an angularjs application to use the new version of angular-ui-router 1.0.14 and stumbled upon a problem when trying to change $stateParams in the resolve of a state.
For example, previously (when using angular-ui-router 0.3.2) modifying $stateParams worked like this:
$stateProvider.state('myState', {
parent: 'baseState',
url: '/calendar?firstAvailableDate',
template: 'calendar.html',
controller: 'CalendarController',
controllerAs: 'calendarCtrl',
resolve: {
availableDates: ['CalendarService', '$stateParams', function(CalendarService, $stateParams) {
return CalendarService.getAvailableDates().then(function(response){
$stateParams.firstAvailableDate = response[0];
return response;
});
}]
}
})
The problem is firstAvailableDate is populated after a resolve and I do not know how to update $transition$.params() during a resolve when usign the new version of angular-ui-router 1.0.14.
I have tried, and managed to update the url parameter with
firing a $state.go('myState', {firstAvailableDate : response[0]}) but this reloads the state, so the screen flickers
modified $transition$.treeChanges().to[$transition$.treeChanges().length-1].paramValues.firstAvailableDate = response[0]; to actually override the parameters. I have done this after looking through the implementation on params() for $transition$.
Although both those options work, they seem to be hacks rather than by the book implementations.
What is the correct approach to use when trying to modify parameters inside a resolve?
Approach with dynamic parameter:
Take a look at this document: params.paramdeclaration#dynamic. Maybe thats what you are looking for: ...a transition still occurs....
When dynamic is true, changes to the parameter value will not cause the state to be entered/exited. The resolves will not be re-fetched, nor will views be reloaded.
Normally, if a parameter value changes, the state which declared that the parameter will be reloaded (entered/exited). When a parameter is dynamic, a transition still occurs, but it does not cause the state to exit/enter.
This can be useful to build UI where the component updates itself when the param values change. A common scenario where this is useful is searching/paging/sorting.
Note that you are not be able to put such logic into your resolve inside your $stateProvider.state. I would do this by using dynamic parameters to prevent the state reload. Unfortunally, the dynamic rules doesn't work when you try to update your state (e.g. by using $stage.go()) inside the resolve part. So I moved that logic into the controller to make it work nice - DEMO PLNKR.
Since userId is a dynamic param the view does not get entered/exited again when it was changed.
Define your dynamic param:
$stateProvider.state('userlist.detail', {
url: '/:userId',
controller: 'userDetail',
controllerAs: '$ctrl',
params: {
userId: {
value: '',
dynamic: true
}
},
template: `
<h3>User {{ $ctrl.user.id }}</h3>
<h2>{{ $ctrl.user.name }} {{ !$ctrl.user.active ? "(Deactivated)" : "" }}</h2>
<table>
<tr><td>Address</td><td>{{ $ctrl.user.address }}</td></tr>
<tr><td>Phone</td><td>{{ $ctrl.user.phone }}</td></tr>
<tr><td>Email</td><td>{{ $ctrl.user.email }}</td></tr>
<tr><td>Company</td><td>{{ $ctrl.user.company }}</td></tr>
<tr><td>Age</td><td>{{ $ctrl.user.age }}</td></tr>
</table>
`
});
Your controller:
app.controller('userDetail', function ($transition$, $state, UserService, users) {
let $ctrl = this;
this.uiOnParamsChanged = (newParams) => {
console.log(newParams);
if (newParams.userId !== '') {
$ctrl.user = users.find(user => user.id == newParams.userId);
}
};
this.$onInit = function () {
console.log($transition$.params());
if ($transition$.params().userId === '') {
UserService.list().then(function (result) {
$state.go('userlist.detail', {userId: result[0].id});
});
}
}
});
Handle new params by using $transition.on* hooks on route change start:
An other approach would be to setup the right state param before you change into your state. But you already said, this is something you don't want. If I would face the same problem: I would try to setup the right state param before changing the view.
app.run(function (
$transitions,
$state,
CalendarService
) {
$transitions.onStart({}, function(transition) {
if (transition.to().name === 'mySate' && transition.params().firstAvailableDate === '') {
// please check this, I don't know if a "abort" is necessary
transition.abort();
return CalendarService.getAvailableDates().then(function(response){
// Since firstAvailableDate is dynamic
// it should be handled as descript in the documents.
return $state.target('mySate', {firstAvailableDate : response[0]});
});
}
});
});
Handle new params by using $transition.on* hooks on route change start via redirectTo
Note: redirectTo is processed as an onStart hook, before LAZY resolves.
This does the same thing as provided above near the headline "Handle new params by using $transition.on* hooks on route change start" since redirectTo is also a onStart hook with automated handling.
$stateProvider.state('myState', {
parent: 'baseState',
url: '/calendar?firstAvailableDate',
template: 'calendar.html',
controller: 'CalendarController',
controllerAs: 'calendarCtrl',
redirectTo: (trans) => {
if (trans.params().firstAvailableDate === '') {
var CalendarService = trans.injector().get('CalendarService');
return CalendarService.getAvailableDates().then(function(response){
return { state: 'myState', params: { firstAvailableDate: response[0] }};
});
}
}
});

can I set ui-router state's data attribute dynamically?

Imagine I have a state definition like this:
$stateProvider.state("main", {
url: "main/:param/list",
controller: "MainController",
data: function($stateParams) {
if($stateParams.param === "A") {
return 'param1';
} else if($stateParams.param === "B") {
return 'param2';
}
}
}
Can I set data attribute like this? Is there any way to achieve the desired behavior?
the data object is a storage for any data you want to add to the $state service, which means you can store the function inside it, but you it won't be executed which i assume not what you're trying to achieve, You can learn about adding the custom data to the $state here
, so to do the logic you wanna do you have to use the controller not the $state service, as follows:
$stateProvider.state("main", {
url: "main/:param/list",
controller: "MainController"/*,
//only if you want to "store" your data on the $state, u can use it like this
data: {
myCustomParam: 'my Awesome Value'
}*/
})//..all your other states
//now you pass the parameter in the url like This
//http://xxxxxxx/main/myparamvalue/list
//and in the controller
app.controller('MainController',function($state, $stateParams){
//Here you do your Logic
if($stateParams.param === "myparamvalue") {
console.log('param1');//it should go here
} else if($stateParams.param === "anythingelse") {
console.log('param2');
}
/*if you have any values to get from the state data as previously mentioned you can get it like this*/
console.log($state.current.data.myCustomParam);//output: my Awesome Value
});

UI Router: How to validate parameter before state resolves

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.

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