UI-router stateParams cloning objects? - angularjs

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.

Related

Where am I doing wrong in converting angularjs component into es6 class?

The options for chart are not coming. $onChanges method is not getting invoked. There is no compilation error though. I have executed the same without es6 convention. When I have converted this into es6 classes, I am facing this issue. Please help...
class chartController{
constructor(dashboardService){
this.dashboardService = dashboardService;
}
$onChanges(changes) {
console.log(changes);
if (changes && changes.options.currentValue) {
this.lineChartOptions = dashboardService.options.getLineChartOptions();
console.log("calling updateLineChartOptions");
updateLineChartOptions();
}
}
updateLineChartOptions() {
angular.extend(this.lineChartOptions, this.options);
this.lineChartOptions.bindingOptions = {
'dataSource': '$ctrl.options.dataSource',
};
this.lineChartOptions.valueAxis = {
valueType: 'numeric'
};
}
}
class ChartComponent{
bindings = {
$transition$: '<', options: '<'
};
controller = chartController;
templateUrl = 'views/chart.html';
}
app.component('chart', ChartComponent);
const ChartComponent = {
bindings: {
$transition$: '<',
options: '<'
},
controller: chartController,
templateUrl: 'views/chart.html',
}
app.component('chart', ChartComponent);
The .component method of an angular.module instance requires an object for its second argument, not a class (or function).
For more information, see
AngularJS angular.module Type API Reference - component method

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

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.

angular ui-router go to URL

How to use $state.go() if I have just the URL ?
Or can I get a state based on URL? (and than use $state.go(state))
I'm asking because I had to intercept the $urlRouterProvider.otherwise() to wait for an other plugin loads some external modules.. and now I need to continue and call the URL that call otherwise()
In place of $state.go(), you can use $location service as well.
i.e.
$location.path(url)
Please take care of not using # in URL. You can also use window.location.href
I had a similar problem, and $location wasn't helping, so I wrote a function to get the state from the url.
NB: I am using nested states based on ui-router.stateHelper, so I traverse my nested states object, testing for url matches. It would be slightly different when using dot notation to define nested states - and even easier if you don't use nested states at all!
function goPath (path) {
var target;
var arr = path.match(/\/\w+/g);
var i = 0;
var testState = function (state, i) {
if (state.url === arr[i]) {
target = state;
if (state.children && state.children.length && arr.length > i+1) {
i++;
state.children.forEach( function (childState) {
testState(childState, i);
});
}
}
};
myStatesObj.forEach( function (state) {
testState(state, i);
});
$state.go(target.name);
};
I was on a similar situation, what I did is changed the location to a different path and reset it to the current after a timeout like this
var path = $location.path();
$location.path("/");
$timeout(function(){
$location.path(path).replace(); //use .replace() so the empty path won't go to the history
},0);
i'm adding a full answer to this due to the high number of views.
NOTE: location.search() is only used where you need to handle a URL with a query string in it. otherwise use location.path() only.
your ui.router login state should look something like ...
.state('login', {
url: '/login',
templateUrl: 'routes/login/login.html',
controller: 'LoginController',
controllerAs: 'loginCtrl',
authenticate: false,
params: {
fwdPath: undefined, // Location to forward to on login success
fwdQueryStringObject: undefined // Query string object to use on login success - retrieved from $location.search()
}
})
your 401 (unauthorised) interceptor should look something like ...
state.go('login', {fwdPath: location.path(), fwdQueryStringObject: location.search()});
your login controllers login function should call your login service's login function. the code INSIDE the controllers login function should look something like ...
loginService.login(self.username, self.password).then(function (response) {
// local vars prevent unit test failure
var fwdPath = state.params.fwdPath;
var fwdQueryStringObject = state.params.fwdQueryStringObject;
if (response.status === 200) {
timeout(function () {
if (fwdPath != null) {
location.path(fwdPath).search(fwdQueryStringObject);
location.replace();
} else {
state.go('home');
}
}, 400);
} else {
self.error = true;
}
self.pending = false;
}
};
and finally your unit tests ...
state.params.fwdPath = '/login/list';
state.params.fwdQueryStringObject = {q: 5};
spyOn(location, 'path').and.callThrough();
spyOn(location, 'search').and.callThrough();
...
expect(location.path).toHaveBeenCalledWith('/login/list');
expect(location.search).toHaveBeenCalledWith({q: 5});

Resources