Related
Here is my code :
Js:
angular.module('main', [])
.config(['$locationProvider', '$routeProvider',
function($locationProvider, $routeProvider) {
$routeProvider.when('/tables/bricks', {
controller: "myController",
resolve: {
"check" : function($location){
if(!$scope.bricks) {
$route.reload();
}
}
},
templateUrl: 'tables/bricks.html'
});
$routeProvider.otherwise({
redirectTo: '/tables/datatables'
});
}
])
.controller('myController', function($scope, $location, $http) {
var vm = this;
$scope.Bricks = function(){
$location.path('/tables/bricks');
};
vm.getbricks = function(n){
var url = n;
$http({
method: 'GET' ,
url: url,
})
.then(function successCallback(data) {
$scope.bricks = data.data;
console.log($scope.bricks);
}, function errorCallback(response) {
console.log(response);
console.log('error');
});
};
});
HTML:
<button ng-click="vm.getbricks(n.bricks_url);Bricks();"></button>
After click the button in html, my page goes into /tables/bricks, but nothing happend, because resolve probably is wrong. What I want - that i could go to /tables/bricks only then, when $scope.bricks exist, so only when vm.bricks() will be called.
Thanks for answers in advance!
I think your problem is that the vm.getbricks will always return something (in success or error handler), so will never be falsy, and you will always call the Bricks() constructor. try to return true on success callback and false in error callback.
$scope is for controllers, which it can't reach in the config. Instead, you should be returning something from a service, which will be called during your resolve. E.g. if(YourService.getbricks())
Solution: move your logic from a controller into a service. And make sure to return a value from it that can be checked in the config.
app.service('BrickService', function() {
this.getbricks = function(url) {
return $http.get(url) // return the Promise
.then(function(response) {
return response.data; // return the data
}, function(error) {
console.log(error);
});
};
});
With this you can inject the service into the config and run its function.
angular.module('main', [])
.config(['$locationProvider', '$routeProvider',
function($locationProvider, $routeProvider) {
$routeProvider.when('/tables/bricks', {
controller: "myController",
resolve: {
"check": function(BrickService) { // inject
if ( BrickService.getbricks() ) { // run its function
$route.reload();
}
}
},
templateUrl: 'tables/bricks.html'
});
$routeProvider.otherwise({
redirectTo: '/tables/datatables'
});
}
])
You can also use the loaded values in the controller after they have been resolved. For that, you would need to simply return it. So change the logic to this:
resolve: {
"check": function(BrickService) { // inject
var bricks = BrickService.getbricks(); // run its function
if ( bricks ) {
$route.reload();
}
return bricks; // return the result (note: it's not a Promise anymore)
}
}
Then you can inject this resolve into your controller:
.controller('myController', function($scope, $location, $http, check) {
var vm = this;
vm.bricks = check;
...
(Note check was added)
I am working on a mobile application that gets a list of jobs from the server (WEBAPI) and populates the proper fields. When the user clicks on the job name, the application goes to a details page, where the job information needs to show again.
I am having issues getting the information to show.
Here is the index:
.state('jobs',{
abstract: true,
url: '/jobs',
templateUrl: 'modules/jobs/views/jobs.html',
controller: ['$scope', '$state', '$stateParams', 'jobs', function($scope, $state, $stateParams, jobs) {
jobs.getData()
.then(function(jobs) {
$scope.jobs = jobs;
});
}]
})
// Jobs > List
.state('jobs.list', {
url: '',
title: 'All Jobs',
templateUrl: 'modules/jobs/views/jobs.list.html'
})
// Jobs > Detail
.state('jobs.detail', {
url: '/{JobId:[0-9]{1,4}}',
title: 'Job Details',
views: {
'details': {
templateUrl: 'modules/jobs/views/jobs.detail.html',
controller: ['$scope', '$state', '$stateParams', 'utils', function($scope, $state, $stateParams, utils) {
$scope.job = utils.findById($scope.jobs, $stateParams.JobId);
$scope.edit = function(){
$state.go('.edit', $stateParams);
};
}]
},
'': {
templateUrl: 'modules/jobs/views/jobs.materials.html',
controller: ['$scope', 'materials', '$stateParams', function($scope, materials, $stateParams) {
materials.getDataById($stateParams.JobId)
.then(function(materials) {
$scope.materials = materials;
});
$scope.subHeader = 'Bulk Sack Materials';
}]
}
}
})
Here is the Service:
app.factory('jobs', ['$resource', '$q', '$http', 'localStorageService', function($resource, $q, $http, localStorageService) {
localStorageService.set('SessionId', 'A00DB328-7F9C-4517-AD5D-8EAA16FBBC8F');
var SessionId = localStorageService.get('SessionId');
return {
getData: function() {
var deferred = $q.defer();
$http.get(baseUrl + 'Job/GetJobs?SessionId=' + SessionId, {
cache: true
}).success(function(jobs) {
deferred.resolve(jobs);
});
return deferred.promise;
}
};
}]);
app.factory('materials', ['$resource', '$q', '$http', 'localStorageService', function($resource, $q, $http, localStorageService) {
var SessionId = localStorageService.get('SessionId');
return {
getDataById: function(id) {
var deferred = $q.defer();
$http.get(baseUrl + 'Material/GetMaterials/' + id + '?SessionId=' + SessionId, {
cached: 'true'
}).success(function(materials) {
deferred.resolve(materials);
});
return deferred.promise;
}
};
}]);
And here is the utils service:
app.factory('utils', function() {
return {
findById: function findById(a, id) {
for (var i = 0; i < a.length; i++) {
if(a[i].id === id) {
return a[i];
}
}
return null;
}
};
});
Here is the HTML for the job.list:
<div class="list-group">
<a class="list-group-item" ng-repeat="job in jobs" ui-sref="jobs.detail({ JobId: job.JobId })">
<dl>
<dt>{{job.Name}}</dt>
<dd>{{job.Location}}</dd>
</dl>
Some insight on how to get this to work would be awesome.
Thank You-
If I have inferred your goal correctly, you're issue is on the following line:
$scope.job = utils.findById($scope.jobs, $stateParams.JobId);
$scope.jobs will not exist like you expect it to. The jobs object was created in the list view's controller's scope, not the details view's controller. You'll want to do something like you have in the '' controller
JobService.getJobById($stateParams.JobId).then(function(data) {
$scope.job = data;
});
I'm new at Javascripts and i'm trying to use Angular UI route, here is my code
myApp.config(['$stateProvider', '$urlRouterProvider',function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('home', {
url: '/test',
templateUrl: '/custom.html'
})
.state('detail', {
url: '/{examID}',
views: {
'': {
templateUrl: '/templates/customize.html',
controller: ['$scope', '$stateParams', 'utils',
function ( $scope,$stateParams,utils) {
$scope.exam = utils.findById($stateParams.examID);
console.log('exam is ' + $scope.exam );
}
]
}
}
} )
}])
and this is the service which has findbyID function
angular.module('service', [])
.factory('utils', function ( $http) {
return {
findById: function findById(id) {
$http.get('/api/exams/' + id).success(function(response) {
return response;
})}
};});
i've already follwed this topic but $scope.exam still undefined
How to return value from an asynchronous callback function?
PS. i've tried to print out response and it's an object
Thx
This is a place where a lot of developers new to JavaScript stumble.
What is going on here is that you are assigning the return value of utils.findById() to $scope.exam. The problem is that utils.findById() doesn't actually return anything. (When a function doesn't have an explicit return statement in JavaScript, the return value is implicitly undefined.)
Here is what your service should look like:
angular
.module('service', [])
.factory('utils', function ($http) {
return {
findById: function (id) {
return $http.get('/api/exams/' + id);
}
};
});
You probably noticed that the call to .success() has disappeared too! Don't worry. It just moved.
Instead of calling .success() on $http.get(), we want to call it on utils.findById(). Doing this will give you access to the response variable in your controller. Because you will have access to the response variable, you will be able to assign response to $scope.exam like so:
.state('detail', {
url: '/{examID}',
views: {
'': {
templateUrl: '/templates/customize.html',
controller: ['$scope', '$stateParams', 'utils',
function ($scope, $stateParams, utils) {
utils.findById($stateParams.examID)
.success(function (response) {
$scope.exam = response;
});
}
]
}
}
});
Hopefully that cleared it up. If I haven't been clear on anything, please let me know so I can update this answer.
You have to wait for the ajax call to finish. Modify the code in your controller to:
$scope.exam;
utils.findById($stateParams.examID).then(function(data) {
$scope.exam = data.data;
}
Read about the concept of 'Promises' in AngularJS and JavaScript.
Use deferred promise, So that it would return value after response
Service:
angular.module('service', [])
.factory('utils', function ( $http) {
return {
findById: function findById(id) {
var promise=$http.get('/api/exams/' + id);
return promise;
};});
Controller:
myApp.config(['$stateProvider',
'$urlRouterProvider',function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('home', {
url: '/test',
templateUrl: '/custom.html'
})
.state('detail', {
url: '/{examID}',
views: {
'': {
templateUrl: '/templates/customize.html',
controller: ['$scope', '$stateParams', 'utils',
function ($scope, $stateParams, utils) {
utils.findById($stateParams.examID).then(function(value) {
$scope.exam = value;
console.log('exam is ' + $scope.exam );
});
}
]
}
}
})
}])
I'm trying to DRY in $stateProvider and prevent adding the same auth function in each resolve. I've created decorator that in each state change would add this function to current state, but auth function isn't invoked, How to fix it or how to workaround discussed issue?
app.config(function ($stateProvider, $urlRouterProvider, $provide) {
$provide.decorator('$state', function($delegate, $rootScope) {
$rootScope.$on('$stateChangeStart', function(event, state, params) {
if ($delegate.current === "login" || $delegate.current === "register") {
return;
}
console.log("decorator", $delegate);
$delegate.current.resolve = {
auth: ['AuthService', '$stateParams', function(AuthService, $stateParams) {
//how to invoke this function?
if (AuthService.isAuthenticated()) {
return AuthService.me(); //promise
} else {
return false;
}
}]
};
});
return $delegate;
});
states definition:
$stateProvider.state('root', {
abstract: true,
url: '/',
views: {
"": {
controller: 'RootCtrl',
templateUrl: 'views/root.html'
},
"header#root": {
templateUrl: 'views/header.html'
}
}
})
.state('root.home', {
url: urlPrefix,
views: {
"content#artworks": {
templateUrl: 'views/home.html',
//resolve: {
// auth: ['AuthService', '$stateParams', function(AuthService, $stateParams) {
// }]
//}
}
}
})
...
If I understand your requirement correctly, we can use native UI-Router built-in decorator:
decorator(name, func)
Allows you to extend (carefully) or override (at your own peril) the stateBuilder object used internally by $stateProvider. This can be used to add custom functionality to ui-router, for example inferring templateUrl based on the state name... (read more in the doc)
There is a working plunker
So, we can have this var auth
var auth = ['AuthService', '$stateParams',
function(AuthService, $stateParams) {
//how to invoke this function on needed states?
if (AuthService.isAuthenticated()) {
return AuthService.me();
} else {
return false;
}
}
];
And here we just use decorator with some "IF" logic
.config(['$stateProvider',
function($stateProvider) {
$stateProvider.decorator('views', function(state, parent) {
var result = {},
views = parent(state);
// some naive example when to not inject resolve
if (state.name === "home") {
return views;
}
// child already has that in parent
if (state.name.indexOf(".") > 0) {
return views;
}
angular.forEach(views, function(config, name) {
// here inject the resolve (if not existing)
config.resolve = config.resolve || {};
// and extend it with the auth stuff above
config.resolve.auth = auth;
result[name] = config;
});
return result;
});
}
])
And later few our states, which will be extended by the above stuff
$stateProvider
.state('home', {
url: "/home",
templateUrl: 'tpl.html',
})
.state('parent', {
url: "/parent",
templateUrl: 'tpl.html',
controller: 'SharedCtrl',
})
.state('parent.child', {
url: "/child",
templateUrl: 'tpl.html',
controller: 'SharedCtrl',
});
Check it in action here
I realized that $delegate.current object contains only raw stateProvider config data. To wrap resolve function I add my function to $delegate.$current on each state change.
$provide.decorator('$state', function($delegate, $rootScope) {
$rootScope.$on('$stateChangeStart', function(event, state, params) {
if ($delegate.current === "err404" || $delegate.current === "login" || $delegate.current === "register") {
return;
}
console.log("decorator", $delegate);
$delegate.$current.resolve["auth"] = ['AuthService', '$stateParams', function(AuthService, $stateParams) {
if (AuthService.isAuthenticated()) {
console.log('AuthService.me()');
return AuthService.me();
} else {
return false;
}
}]
});
return $delegate;
});
Update
I found related discussion on github, you can add universal resolve function into toState param:
app.run(['$rootScope', '$state',
function($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function(event, toState) {
if (toState.name === "login" || toState.name === "register") {
return;
}
toState["resolve"]["auth"] = ['AuthService', '$stateParams', function(AuthService, $stateParams) {
if (AuthService.isAuthenticated()) {
return AuthService.me();
} else {
return false;
}
}];
});
}
]);
I am trying to create a "Todo App" with angularjs ui-router. It has 2 columns:
Column 1: list of Todos
Column 2: Todo details or Todo edit form
In the Edit and Create controller after saving the Todo I would like to reload the list to show the appropriate changes. The problem: after calling $state.go('^') when the Todo is created or updated, the URL in the browser changes back to /api/todo, but the ListCtrl is not executed, i.e. $scope.search is not called, hence the Todo list (with the changed items) is not retrieved, nor are the details of the first Todo displayed in Column 2 (instead, it goes blank).
I have even tried $state.go('^', $stateParams, { reload: true, inherit: false, notify: false });, no luck.
How can I do a state transition so the controller eventually gets executed?
Source:
var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/api/todo');
$stateProvider
.state('todo', {
url: '/api/todo',
controller: 'ListCtrl',
templateUrl: '/_todo_list.html'
})
.state('todo.details', {
url: '/{id:[0-9]*}',
views: {
'detailsColumn': {
controller: 'DetailsCtrl',
templateUrl: '/_todo_details.html'
}
}
})
.state('todo.edit', {
url: '/edit/:id',
views: {
'detailsColumn': {
controller: 'EditCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
.state('todo.new', {
url: '/new',
views: {
'detailsColumn': {
controller: 'CreateCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
;
})
;
TodoApp.factory('Todos', function ($resource) {
return $resource('/api/todo/:id', { id: '#id' }, { update: { method: 'PUT' } });
});
var ListCtrl = function ($scope, $state, Todos) {
$scope.todos = [];
$scope.search = function () {
Todos.query(function (data) {
$scope.todos = $scope.todos.concat(data);
$state.go('todo.details', { id: $scope.todos[0].Id });
});
};
$scope.search();
};
var DetailsCtrl = function ($scope, $stateParams, Todos) {
$scope.todo = Todos.get({ id: $stateParams.id });
};
var EditCtrl = function ($scope, $stateParams, $state, Todos) {
$scope.action = 'Edit';
var id = $stateParams.id;
$scope.todo = Todos.get({ id: id });
$scope.save = function () {
Todos.update({ id: id }, $scope.todo, function () {
$state.go('^', $stateParams, { reload: true, inherit: false, notify: false });
});
};
};
var CreateCtrl = function ($scope, $stateParams, $state, Todos) {
$scope.action = 'Create';
$scope.save = function () {
Todos.save($scope.todo, function () {
$state.go('^');
});
};
};
I would give an example (a draft) of HOW TO nest edit into detail. Well, firstly let's amend the templates.
The Detail template, contains full definition of the detail. Plus it now contains the attribute ui-view="editView". This will assure, that the edit, will "replace" the detail from the visibility perspective - while the edit scope will inherit all the detail settings. That's the power of ui-router
<section ui-view="editView">
<!-- ... here the full description of the detail ... -->
</section>
So, secondly let's move the edit state, into the detail
// keep detail definition as it is
.state('todo.details', {
url: '/{id:[0-9]*}',
views: {
'detailsColumn': {
controller: 'DetailsCtrl',
templateUrl: '/_todo_details.html'
}
}
})
// brand new definition of the Edit
.state('todo.details.edit', { // i.e.: url for detail like /todo/details/1/edit
url: '/edit',
views: {
'editView': { // inject into the parent/detail view
controller: 'EditCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
Having this adjusted state and template mapping, we do have a lot. Now we can profit from the ui-router in a full power.
We'll define some methods on a DetailCtrl (remember, to be available on the inherit Edit state)
var DetailsCtrl = function ($scope, $stateParams, Todos) {
$scope.id = $stateParams.id // keep it here
// model will keep the item (todos) and a copy for rollback
$scope.model = {
todos : {},
original : {},
}
// declare the Load() method
$scope.load = function() {
Todos
.get({ id: $stateParams.id })
.then(function(response){
// item loaded, and its backup copy created
$scope.model.todos = response.data;
$scope.model.original = angular.copy($scope.model.todos);
});
};
// also explicitly load, but just once,
// not auto-triggered when returning back from Edit-child
$scope.load()
};
OK, it should be clear now, that we do have a model with the item model.todos and its backup model.original.
The Edit controller could have two actions: Save() and Cancel()
var EditCtrl = function ($scope, $stateParams, $state, Todos) {
$scope.action = 'Edit';
// ATTENTION, no declaration of these,
// we inherited them from parent view !
//$scope.id .. // we DO have them
//$scope.model ...
// the save, then force reload, and return to detail
$scope.save = function () {
Todos
.update({ id: id })
.then(function(response){
// Success
$scope.load();
$state.go('^');
},
function(reason){
// Error
// TODO
});
};
// a nice and quick how to rollback
$scope.cancel = function () {
$scope.model.todos = Angular.copy($scope.model.original);
$state.go('^');
};
};
That should give some idea, how to navigate between parent/child states and forcing reload.
NOTE in fact, instead of Angular.copy() I am using lo-dash _.cloneDeep() but both should work
Huge thanks for Radim Köhler for pointing out that $scope is inherited. With 2 small changes I managed to solve this. See below code, I commented where I added the extra lines. Now it works like a charm.
var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/api/todo');
$stateProvider
.state('todo', {
url: '/api/todo',
controller: 'ListCtrl',
templateUrl: '/_todo_list.html'
})
.state('todo.details', {
url: '/{id:[0-9]*}',
views: {
'detailsColumn': {
controller: 'DetailsCtrl',
templateUrl: '/_todo_details.html'
}
}
})
.state('todo.edit', {
url: '/edit/:id',
views: {
'detailsColumn': {
controller: 'EditCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
.state('todo.new', {
url: '/new',
views: {
'detailsColumn': {
controller: 'CreateCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
;
})
;
TodoApp.factory('Todos', function ($resource) {
return $resource('/api/todo/:id', { id: '#id' }, { update: { method: 'PUT' } });
});
var ListCtrl = function ($scope, $state, Todos) {
$scope.todos = [];
$scope.search = function () {
Todos.query(function (data) {
$scope.todos = $scope.todos(data); // No concat, just overwrite
if (0 < $scope.todos.length) { // Added this as well to avoid overindexing if no Todo is present
$state.go('todo.details', { id: $scope.todos[0].Id });
}
});
};
$scope.search();
};
var DetailsCtrl = function ($scope, $stateParams, Todos) {
$scope.todo = Todos.get({ id: $stateParams.id });
};
var EditCtrl = function ($scope, $stateParams, $state, Todos) {
$scope.action = 'Edit';
var id = $stateParams.id;
$scope.todo = Todos.get({ id: id });
$scope.save = function () {
Todos.update({ id: id }, $scope.todo, function () {
$scope.search(); // Added this line
//$state.go('^'); // As $scope.search() changes the state, this is not even needed.
});
};
};
var CreateCtrl = function ($scope, $stateParams, $state, Todos) {
$scope.action = 'Create';
$scope.save = function () {
Todos.save($scope.todo, function () {
$scope.search(); // Added this line
//$state.go('^'); // As $scope.search() changes the state, this is not even needed.
});
};
};
I might have faced a similar problem the approach i took was to use $location.path(data.path).search(data.search); to redirect the page then in the controller I caught the $locationChangeSuccess event. I other words I use the $location.path(...).search(...) as apposed to $state.go(...) then caught the $locationChangeSuccess event which will be fired when the location changes occurs before the route is matched and the controller invoked.
var TodoApp = angular.module('TodoApp', ['ngResource', 'ui.router'])
.config(function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/api/todo');
$stateProvider
.state('todo', {
url: '/api/todo',
controller: 'ListCtrl',
templateUrl: '/_todo_list.html'
})
.state('todo.details', {
url: '/{id:[0-9]*}',
views: {
'detailsColumn': {
controller: 'DetailsCtrl',
templateUrl: '/_todo_details.html'
}
}
})
.state('todo.edit', {
url: '/edit/:id',
views: {
'detailsColumn': {
controller: 'EditCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
.state('todo.new', {
url: '/new',
views: {
'detailsColumn': {
controller: 'CreateCtrl',
templateUrl: '/_todo_edit.html'
}
}
})
;
})
;
TodoApp.factory('Todos', function ($resource) {
return $resource('/api/todo/:id', { id: '#id' }, { update: { method: 'PUT' } });
});
var ListCtrl = function ($scope, $state, Todos, todo.details) {
/*here is where i would make the change*/
$scope.$on('$locationChangeSuccess', function () {
$scope.search();
$route.reload();
});
$scope.todos = [];
$scope.search = function () {
Todos.query(function (data) {
$scope.todos = $scope.todos.concat(data);
});
};
$scope.search();
};
var DetailsCtrl = function ($scope, $stateParams, Todos) {
$scope.todo = Todos.get({ id: $stateParams.id });
};
var EditCtrl = function ($scope, $stateParams, $state, Todos, $location) {
$scope.action = 'Edit';
var id = $stateParams.id;
$scope.todo = Todos.get({ id: id });
$scope.save = function () {
Todos.update({ id: id }, $scope.todo, function () {
//here is where I would make a change
$location.path('todo.details').search($stateParams);
});
};
};
var CreateCtrl = function ($scope, $stateParams, $state, Todos, $location) {
$scope.action = 'Create';
$scope.save = function () {
Todos.save($scope.todo, function () {
//here is where I would make a change
$location.path('todo.details');
});
};
};
the $locationChangeSuccess event occurs before the route is matched and the controller invoked