Angular ui-router struggle - angularjs

Well, I have this project, and ui-router is giving me hard times. I made a quick Plunker demo: http://plnkr.co/edit/imEErAtOdEfaMMjMXQfD?p=preview
So basically I have a main view, index.html, into which other top-level views get injected, like this operations.index.html. The pain in my brain starts when there are multiple named views in a top-level view, operations.detail.html and operations.list.html are injected into operations.index.html, which is in turn injected into index.html.
Basically, what I'm trying to achieve is the following behaviour:
When a user clicks Operations item in the navbar, a page with empty (new) operation is shown. The URL is /operations.
When they select an item in a list, the fields are updated with some data (the data is requested via a service, but for simplicity let's assume it's right there in the controller). The URL is /operations/:id.
If they decide that they want to create a new item, while editing a current one, they click New operation button on top of the list, the URL changes from /operations/:id to /operations.
No matter new or old item, the item Operations in the navbar stays active.
If the user is editing an item, it should be highlighted as active in the list, if they create a new item — New operation button should be highlighted, accordingly.
Now, check out the weird behaviour: go to Operations and then click Operations navbar item again. Everything disappears. The same happens if I do Operations -> select Operation 1 -> select New operation.
Besides, please, check out the part where I try to get the id parameter:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if (toParams) {
if (toParams.id) {
for (var i = 0; i < vm.operations.length; i++) {
if (vm.operations[i].id == toParams.id) {
vm.operation = vm.operations[i];
break;
}
}
}
}
});
I am no expert, but it seems too long and complex to be true, especially for such a simple task as getting a request parameter. If I try to check on state change $stateParams the object is empty, hence this workaround. If I try to tamper with states in app.js, things change slightly, but there are always bugs like Operations navbar item losing its active state or other weird stuff.
I know that asking such general questions is uncommon in SO, but I really can't grasp the concept of the ui-router, and I can feel that I'm doing things wrong here, and I would really appreciate any help in pointing me in the right direction of how to properly use ui-router for my purposes. Cheers.

There is the updated plunker
I just used technique from this Q & A: Redirect a state to default substate with UI-Router in AngularJS
I added the redirectTo setting (could be on any state)
.state('operations', {
url: '/operations',
templateUrl: 'operations.index.html',
controller: 'operationsController as op',
// here is redirect
redirectTo: 'operations.new',
})
and added this redirector:
app.run(['$rootScope', '$state', function($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function(evt, to, params) {
if (to.redirectTo) {
evt.preventDefault();
$state.go(to.redirectTo, params)
}
});
}]);
and also, I removed the redirection currently sitting in the operationsController.js:
angular.module('uiRouterApp')
.controller('operationsController', function($state, $stateParams, $rootScope) {
var vm = this;
//if ($state.current.name === 'operations') $state.go('operations.new');
And that all above is just to keep the new state - without url. Because the solution would become much more easier, if we would just introduce url: '/new':
.state('operations', {
url: '/operations',
..
})
.state('operations.new', {
//url: '',
url: '/new',
Check the plunker here
So, this way we gave life to our routing. Now is time to make the detail working. To make it happen we would need more - there is another updated plunker
Firstly, we will get brand new controller to both child state views:
.state('operations.new', {
url: '',
views: {
'detail': {
templateUrl: 'operations.detail.html',
controller: 'detailCtrl as dc', // here new controller
...
})
.state('operations.detail', {
url: '/:id',
views: {
'detail': {
templateUrl: 'operations.detail.html',
controller: 'detailCtrl as dc', // here new controller
...
It could be same controller for both, because we will keep decision new or existing on the content of the $stateParams.id. This would be its implementation:
.controller('detailCtrl', function($scope, $state, $stateParams) {
var op = $scope.op;
op.operation = {id:op.operations.length + 1};
if ($stateParams.id) {
for (var i = 0; i < op.operations.length; i++) {
if (op.operations[i].id == $stateParams.id) {
op.operation = op.operations[i];
break;
}
}
}
})
We keep the original approach, and set the op.operation just if $stateParams.id is selected. If not, we create new item, with id properly incremented.
Now we just adjust parent controller, to not save existing, just new:
.controller('operationsController', function($state, $stateParams, $rootScope) {
var vm = this;
//if ($state.current.name === 'operations') $state.go('operations.new');
vm.operation = {};
/*$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams) {
if (toParams) {
if (toParams.id) {
for (var i = 0; i < vm.operations.length; i++) {
if (vm.operations[i].id == toParams.id) {
vm.operation = vm.operations[i];
break;
}
}
}
}
});*/
vm.save = function() {
if(vm.operations.indexOf(vm.operation) >= 0){
return;
}
if (vm.operation.name
&& vm.operation.description
&& vm.operation.quantity) {
vm.operations.push(vm.operation);
vm.operation = {id: vm.operations.length + 1};
}
};
Check the complete version here

Related

calling controller function from within a promise in external JS

Ionic Tabs, root of tabs HTML has "RootTabCtrl", and "Tab1" (with "Tab1_Ctrl") has a form, other tabs are disabled.
User submits form on Tab1
Tab1 Controller function kicks off.
Controller Function calls an external function (not in controller).
External function triggers, which executes a promise
In promise "results", returned data is processed
If X in returned data is true, trigger "RootTabCtrl" function to enable the other disabled tabs.
I can track console messages triggering every step of the way.
This all works except for this following odd behavior. "RootTabCtrl" doesn't enable the disabled tabs until the user clicks the form submit a second time...even though I see console messages saying it is in (at the end) of the RootTabCtrl function. I see all the same console messages from the first click - but on the 2nd time is when the disabled tabs get enabled again.
If I move step 6 outside of the promise in Step 4, and put it after step 3 (and before the promise), then all the tabs get enabled on the 1st click. However, this is no longer taking into account the value of X to determine if other tabs should be re-enabled or not.
What can I look for, or am not aware of, that would be causing this?
app.js:
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html",
controller: 'TabsCtrl'
})
// Each tab has its own nav history stack:
.state('tab.tab1', {
url: '/map',
views: {
'tab-1': {
templateUrl: 'templates/tab-1.html',
controller: 'tab1Ctrl'
}
}
})
controllers:
.controller('TabsCtrl', function($scope,$rootScope,constants) {
$scope.constants = constants ;
$scope.tabControl = {
disableRides : true,
disableBikes : true,
disableTransit : true
}
var refreshFinalizer = $rootScope.$on('updateTabsRefresh', function (event, data) {
console.log("Refresher 1") ;
$scope.tabControl.disableTab2 = false;
$scope.tabControl.disableTab3 = false ;
console.log("Refresher 2") ;
});
$scope.$on('$destroy', function () {
console.log("Destroy") ;
refreshFinalizer ();
});
})
.controller('tab1Ctrl', function($scope,$rootScope) {
$scope.setInfo= function() {
getGoogle(document.getElementById('form_data').value,0);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1") ;
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2") ;
}
})
Tab1 has a form, upon click, it executes $scope.setInfo. Then getGoogle() is in an outside JS function, its a call to google maps, and if specific data X is true, then enable all the other tabs using tab1Ctrl $scope.enableTabs() :
function getGoogle(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[0]) {
// extra code removed
startPointSet = 1;
// tab1Ctrl html has id of "Tab1"
angular.element(document.getElementById('Tab1')).scope().enableTabs();
/* alternative method, worked the same as previous line but with same problem
var $sBody = angular.element(document.body) ;
var $sRootScope = $sBody.injector().get('$rootScope') ;
$sRootScope.$broadcast('updateTabsRefresh') ;
*/
console.log("setInfo 2") ;
}
} else {
// extra code removed
console.log(response) ;
}
});
}
}
All of the above works...except that when the call to enableTabs (within the google response), even though it correctly calls enableTabs and I can see the console.log messages firing from within enableTabs - the other tabs don't "enable" until the form button is clicked a 2nd time (and then I see all the console messages again). I tried 2 different methods, from within getGoogle(), both worked exactly the same - 1st clicked fired all functions correctly, but tabs did not enable. 2nd click fired all the functions and then the tabs got enabled.
Try this previous answer. Are you using $http calls? If so I have never actually had to do that so it seems like something else might be the root cause.
UPDATE
How about creating an Angular Service for this GetGoogle function call. Then you will still be inside of angular and can inject $rootScope and anything else that you need. You will need to inject this service into your tab1Ctrl (the myGoggleService below). I would also probably just pass the form back on the ng-submit:
Html
ng-submit="setInfo(formNameGoesHere)"
Controller
.controller('tab1Ctrl', function($scope, $rootScope, myGoogleService) {
$scope.setInfo = function(form) {
myGoogleService.getGoogle(form);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1");
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2");
}
});
Service: If you haven't created any already you will need to register it in your app.js like any directives you have and also put them in your index.html page like a regular controller.
.service('myGoggleService', [ '$rootScope', function ($rootScope) {
this.getGoogle = function(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
$rootScope.$broadcast("updateTabsRefresh");
} else {
}
});
}
}}]);
I did copy your code from above and then just removed some of the extra stuff just to get the point across. Obviously could not run the code so there might be some mistakes or slight changes needed, but hopefully this will get you close.

URL parameters disappears(removed) after changing

As the video shows, the changed url parameter disappears(cleaned/removed) after changing.
https://youtu.be/06aHXdNl7J8
For example, when I choose Taipei in the dropdown, the url should be
http://localhost:3003/user/quick_search/index?departure_name=TAIPEI
But it only persists for a short time,
Then it returned to the original url http://localhost:3003/user/quick_search/index
For my use case, the parameters are multiple and optional, I'm doing the 2-way binding of URL parameters and FORM control
update
For example, If I choose CHIANGMAI as the departure, it seems working at first. The url will be http://133.130.101.114:3000/user/quick_search/index#/?departure_name=CHIANGMAI
However, it will quickly revert to http://133.130.101.114:3000/user/quick_search/index#/
This is the demo site http://133.130.101.114:3000/user/quick_search/index#/
App config
app.config(['$routeProvider', '$locationProvider', function(routeProvider, locationProvider) {
routeProvider.
when('/',
{
templateUrl: 'quick_search/form.html',
controller: 'form_ctrl'
}
).
otherwise(
{
templateUrl: 'quick_search/form.html',
controller: 'form_ctrl'
}
);
locationProvider.html5Mode({
enabled: true,
});
}]);
Form_Controller.js (#change URL params after changing dropdown option)
$scope.updateDeparture = function(departure){
if ( ! (typeof departure === 'undefined' || departure === null) ){
$scope.departure_name = departure.name;
}
}
$scope.$watch("departure_name", function(newValue) {
$location.search("departure_name", newValue);
});

Angular UI Router Reload Controller on Back Button Press

I have a route that can have numerous optional query parameters:
$stateProvider.state("directory.search", {
url: '/directory/search?name&email',
templateUrl: 'view.html',
controller: 'controller'
When the user fills the form to search the directory a function in the $scope changes the state causing the controller to reload:
$scope.searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
};
In the controller I have a conditional: if($state.params){return data} dictating whether or not my service will be queried.
This works great except if the user clicks the brower's forward and/or back buttons. In both these cases the state (route) changes the query parameters correctly but does not reload the controller.
From what I've read the controller will be reloaded only if the actual route changes. Is there anyway to make this example work only using query parameters or must I use a changing route?
You should listen to the event for succesful page changes, $locationChangeSuccess. Checkout the docs for it https://docs.angularjs.org/api/ng/service/$location.
There is also a similar question answered on so here How to detect browser back button click event using angular?.
When that event fires you could put whatever logic you run on pageload that you need to run when the controller initializes.
Something like:
$rootScope.$on('$locationChangeSuccess', function() {
$scope.searchDirectory()
});
Or better setup like:
var searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
$scope.searchDirectory = searchDirectory;
$rootScope.$on('$locationChangeSuccess', function() {
searchDirectory();
});
Using the above, I was able to come up with a solution to my issue:
controller (code snippet):
...var searchDirectory = function (searchParams) {
if (searchParams) {
$scope.Model.Query.name = searchParams.name;
$scope.Model.Query.email = searchParams.email;
}
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email,
}, { reload: true });
};...
$rootScope.$on('$locationChangeSuccess', function () {
//used $location.absUrl() to keep track of query string
//could have used $location.path() if just interested in the portion of the route before query string params
$rootScope.actualLocation = $location.absUrl();
});
$rootScope.$watch(function () { return $location.absUrl(); }, function (newLocation, oldLocation) {
//event fires too often?
//before complex conditional was used the state was being changed too many times causing a saturation of my service
if ($rootScope.actualLocation && $rootScope.actualLocation !== oldLocation && oldLocation !== newLocation) {
searchDirectory($location.search());
}
});
$scope.searchDirectory = searchDirectory;
if ($state.params && Object.keys($state.params).length !== 0)
{..call to service getting data...}
This solution feels more like a traditional framework such as .net web forms where the dev has to perform certain actions based on the state of the page. I think it's worth the compromise of having readable query params in the URL.

How to reuse controllers in AngularJs when success locations are different and $location.path('..') is not supported?

Right now the $location service is getting in the way. Suppose one wants to use the same controller for multiple routes, however the expectation is that upon a successful 'save' the destination routes would be different.
.when('/sponsors/:sponsorId/games/add', {templateUrl: 'partials/games/create',controller: 'GameCreateCtrl', access: 'sponsor'})
// an admin can see all the games at
.when('/admin/games/add', {templateUrl: 'partials/games/create',controller: 'GameCreateCtrl', access: 'admin'})
A game is is displayed on success of either action. The route is just the parent path.
e.g. /admin/games or /sponsors/:sponsorId/games.
The $location service does not seem to support the relative path $location.path('..'). Should it?
What is the best way to reuse the GameCreateCtrl in this situation?
$scope.save = function () {
GameService.save($scope.game).$promise.then(function(res){
console.log(res);
growl.addSuccessMessage("Successfully saved game: " + $scope.game.name);
console.log("saving game by id:" + $scope.game._id);
var path = $location.path();
$location.path(path.replace('/add', '')); // this seems like a hack
});
}
You can do it with resolve:
.when('/sponsors/:sponsorId/games/add', {
templateUrl: 'partials/games/create',
controller: 'GameCreateCtrl',
resolve: {
returnUrl: function($routeParams){
return '/sponsors/' + $routeParams.sponsorId + '/games';
}
}
})
.when('/admin/games/add', {
templateUrl: 'partials/games/create',
controller: 'GameCreateCtrl',
resolve: {
returnUrl: function(){
return '/admin/games';
}
}
})
In controller:
app.controller('myCtrl', function($scope, returnUrl){
$scope.save = function () {
GameService.save($scope.game).$promise.then(function(res){
// ...
$location.path(returnUrl); // this seems like a hack
});
};
});
You are passing different returnUrl parameter to controller depending on route.
I would like to thank the poster karaxuna with their solution. Its the answer I am accepting. However, it is often helpful to have other options at ones disposal.
Another way to solve this would be to create a global function.
function getParentPath($location) {
if ($location.path() != '/') /* can't move up from root */ {
var pathArray = $location.path().split('/');
var parentPath = "";
for (var i = 1; i < pathArray.length - 1; i++) {
parentPath += "/";
parentPath += pathArray[i];
}
return parentPath;
}
}
This works very where when edits/adds follow a rest style with regards to route locations. In these cases the parentPath would always go back to the plural listing of all records.
and perhaps add a method to root scope
$rootScope.goParentPath = function ($location) {
$location.path(getParentPath($location));
}
function inside controllers could call the getParentPath function. e.g.
$scope.cancel = function() {
$scope.goParentPath($location)
}
I am actually leaning considering an approach that combines the first answer with a getParentPath is some situations.
For a little bit of brevity, the routes would make use of the resolve callout, but use the parentPath function in many cases. ex:
.when('/admin/games/:id', {templateUrl: 'partials/games/edit', controller: 'EditGameCtrl', access: 'admin',resolve:{returnUrl: getParentPath}})
.when('/admin/games/add', {templateUrl: 'partials/games/create', controller: 'EditGameCtrl', access: 'admin', resolve:{returnUrl: getParentPath}})

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