AngularJS - Pass parameters into Controller? - javascript

I'm trying to create a simple blog website using AngularJS. I'm just starting out, so what I'm thinking my not be the best way to do this, so any alternative suggestions are welcome.
I have a controller.js file with two blog controllers. One to display a list of blog posts, and the other that displays the post content by including an HTML file.
controller.js
myAppControllers.controller('BlogListCtrl', ['$scope', '$http', function ($scope, $http) {
$http.get('articles/articles.json').success(function (articles) {
$scope.articles = articles;
});
}]);
myAppControllers.controller('BlogPostCtrl', ['$scope', '$routeParams', function ($scope, $routeParams) {
$scope.includeFile = 'articles/' + $routeParams.blogPostId + '.html';
}]);
articles.json
[
{
"id": "test-article-one",
"title": "Test Article one",
"author": "Gareth Lewis",
"datePosted": "2015-06-23",
"summary": "This is a test summary"
},
{
"id": "test-article-two",
"title": "Test article two",
"author": "Gareth Lewis",
"datePosted": "2015-06-23",
"summary": "This is a test for article two"
}
]
app.js
when('/blog', {
templateUrl: 'partials/blog-articles.html',
controller: 'BlogListCtrl'
}).
when('/blog/:blogPostId', {
templateUrl: 'partials/blog-post.html',
controller: 'BlogPostCtrl'
}).
blog-post.html
<ng-include src="'partials/header.html'"></ng-include>
<!-- Want to add title, author, datePosted information here... -->
<article class="content">
<ng-include src="includeFile"></ng-include>
</article>
This blog listings work fine. When I click into a blog post, it also serves up the content from the HTML file OK as well. However, I want to be able to reuse the title, author and datePosted properties from the selected article in the blog-post.html partial view. What's the best way to do this? Would I need to pass them to the Controller somehow to then pass to the view? I don't really want to pass these as routeParams. Or would I need to do a $http.get on articles.json and iterate through to find the selected article and then pass the property values back to the view?
Thanks for the help.

You said that suggestions are welcome, so here it goes.
1 - Transport all your Blog logic to a service;
2 - Provide the data on resolving routes. This is a better approach to handle errors during the load time, 404s, and so on. You can provide a listener to $routeChangeError and deal with it there;
3 - On the service declared below, you have the methods to call your data and a method to retrieve the list cached on the service:
// services.js
myAppServices
.service('BlogService', ['$http', '$q', function ($http, $q) {
var api = {},
currentData = {
list: [],
article: {}
};
api.getSaved = function () {
return currentData;
};
api.listArticles = function () {
var deferred = $q.defer(),
backup = angular.copy(currentData.list);
$http.get('articles/articles.json')
.then(function (response) {
currentData.list = response;
deferred.resolve(response);
}, function () {
currentData.list = backup;
deferred.reject(reason);
});
return deferred.promise;
};
api.getArticle = function (id) {
var deferred = $q.defer(),
backup = angular.copy(currentData.article),
path = 'articles/' + id + '.html';
$http.get(path, {
cache: true
})
.then(function (response) {
currentData.article = {
path: path,
response: response
};
deferred.resolve(currentData.article);
}, function (reason) {
currentData.article = backup;
deferred.reject(currentData.article);
});
return deferred.promise;
};
return api;
}]);
The BlogService.getSaved() will retrieve the stored data, made after each call.
I've made a method to call the ng-include path too, so you can verify if it exists, with cache === true, the browser will keep a copy of it, when calling it again on the view. A copy of the response of the blog article is made too, so you can access its path and the response whenever you need.
On the controllers below, they were adaptated to supply the current needs:
// controller.js
myAppControllers
.controller('BlogListCtrl', ['$scope', 'articles',
function ($scope, articles) {
$scope.articles = articles;
/* OTHER STUFF HERE */
}
])
.controller('BlogPostCtrl', ['$routeParams', '$scope', 'article' 'BlogService',
function ($routeParams, $scope, article, BlogService) {
// On `article` dependency, you have both the original response
// and the path formed. If you want to use any of it.
$scope.includeFile = article.path;
// To get the current stored data (if any):
$scope.articles = BlogService.getSaved().list;
// Traverse the array to get your current article:
$scope.article = $scope.articles.filter(function (item) {
return item.id === $routeParams.id;
});
/* OTHER STUFF HERE */
}
]);
And the route declarations were changed to load the data when resolving the routes.
// app.js
$routeProvider
.when('/blog', {
templateUrl: 'partials/blog-articles.html',
controller: 'BlogListCtrl',
resolve: {
articles: ['BlogService', '$routeParams', function (BlogService, $routeParams) {
return BlogService.listArticles();
}]
}
})
.when('/blog/:id', {
templateUrl: 'partials/blog-post.html',
controller: 'BlogPostCtrl',
resolve: {
article: ['BlogService', '$routeParams', function (BlogService, $routeParams) {
return BlogService.getArticle($routeParams.blogPostId);
}]
}
})

This is maybe a common question in angular. What you have to understand is that Scope is defined per controller... In order to share data across controller you still have the option to use $scope.$parent or $rootScope to link controllers but I would use those carefully.
It is better to use Angular Services which are based on singleton patterns therefore you can use them to share information between controllers and I think it will be a better approach.
I found that this has been previously discussed and here are some good examples:
AngularJS Service Passing Data Between Controllers

You can use a global scope to set this data, or you can use service to communicate between the controllers. There is a lot of ways to resolve this problem read a little bit more about services in the link bellow and see if you can find how to resolve your problem.
AngularJS: Service vs provider vs factory

Related

Redirect to another view by a post id, not working

I am receiving an external object from WordPress, in one view I have the post.title, and if you click in that title you can go to another view and see te entire post.content.
So far, I can not see the entire post because I am getting a couple errors, posts is undefined.
I made a Plunkr, CodePen and one JSBin for you to understand easier. If you use JSBin is better because you can use the console which is integrated there. All you have to do is click on the title of the post, and you are going to realize that can not go to the other view.
Here is the code regarding my issue, which is the same you will see in the online editors I post above
.state('tabs', {
url: "/tabs",
abstract: true,
templateUrl: "tabs.html"
})
.state('tabs.news', {
url: "/news",
views: {
'tab-news': {
templateUrl: "tab-news.html",
controller: 'NewsCtrl'
}
}
})
.state('tabs.post-detail', {
url: '/news/:postId',
views: {
'tab-news': {
templateUrl: 'tab-post-detail.html',
controller: 'PostDetailCtrl'
}
}
})
the html for the main view, news
<a ng-href="#/tabs/news/{{post.ID}}">
<h2 ng-bind-html="post.title"></h2>
<p>{{post.date | date}}</p>
</a>
and here is the view where you can not enter yet, the view where are redirected after clicking in the title on the main view
<div>
<h3>{{:: post.title}}</h3>
<p>{{:: post.content}}</p>
</div>
now the controller for the main view
.controller('NewsCtrl', function($scope, $ionicLoading, FreshlyPressed) {
$scope.posts = [];
$scope.doRefresh = function() {
$scope.posts = FreshlyPressed.getBlogs($scope);
}
});
here the service
angular.module('urbanet.app.service', [])
.service('FreshlyPressed', function($http) {
return {
getBlogs: function($scope) {
$scope.posts = [];
$http.jsonp('https://public-api.wordpress.com/rest/v1.1/freshly-pressed?callback=JSON_CALLBACK')
.success(function(result) {
$scope.posts = result.posts;
});
},
get: function(postId, $scope) {
console.log(postId);
console.log($scope.posts);
for (var i = 0; i < $scope.posts.length; i++) {
if ($scope.posts[i].id === parseInt(postId)) {
return $scope.posts[i];
}
}
return null;
}
}
})
and controller for the second view, the view of the entire post
.controller('PostDetailCtrl', function($scope, $stateParams, FreshlyPressed) {
$scope.post = FreshlyPressed.get($stateParams.postId, $scope);
});
You are going to have to make a new request for individual posts
The freshly-pressed api returns to you a siteID and postId.
You then combine those to use the posts API to get the single post.
Since freshly-pressed is a constantly changing list, this is the only way you would ever be able to bookmark posts. Tomorrow you may not get the same set of main posts, so wouldn't be able to replicate links that were used today the way you are attempting to
Service method:
getPostById: function(siteId,postId ) {
var url ='https://public-api.wordpress.com/rest/v1.1/sites/'+siteId+'/posts/'+postId+'?callback=JSON_CALLBACK'
return $http.jsonp(url)
}
controller
.controller('PostDetailCtrl', function($scope, $stateParams, FreshlyPressed) {
var postId = $stateParams.postId,
siteId = $stateParams.siteId;
FreshlyPressed.getPostById(siteId,postId).success(function(response){
$scope.post = response
})
});
Modify links accordingly
<a ng-href="#/tabs/news/{{post.site_ID}}/{{post.ID}}">
Working demo
.controller('PostDetailCtrl', function($scope, $stateParams, FreshlyPressed) {
$scope.post = FreshlyPressed.get($stateParams.postId, $scope);
});
This is a problem. You are trying to pass the entire $scope object as a parameter?
What exactly do you want to pass to the get call?
You have this:
get: function(postId, $scope) { //<- that shouldn't be $scope, make it 'post' or something
console.log(postId);
console.log($scope.posts); //Notice that this comes back undefined?
for (var i = 0; i < $scope.posts.length; i++) { //<- length throws an error because there isn't anything there.
if ($scope.posts[i].id === parseInt(postId)) {
return $scope.posts[i];
}
}
return null;
}
You shouldn't be trying to pass $scope as a param to your service then access $scope inside the factory.
Have your service store your posts data and return that data to your controllers. You can do something like this:
How to make multiple http requests?
Don't try to pass in $scope. Just send the post id to the service and return the data you need.
As #tpie and #charlietfl already mentioned, the current approach (passing $scope as parameter to the service) doesn't work. Another alternative to what tpie suggested (caching the posts data in the service) might be using resolve and promises:
Service code
.service('FreshlyPressed', function($q, $http) {
return {
getBlogs: function() {
var deferred = $q.defer();
$http.jsonp('https://public-api.wordpress.com/rest/v1.1/freshly-pressed?callback=JSON_CALLBACK')
.success(function(result) {
deferred.resolve(result.posts);
});
return deferred.promise;
},
get: function (postId, posts) {
/* snipped, this wasn't the problematic part */
}
}
})
This won't cache the posts result in your service, which might be a downside if the $http call you are making is heavy, but using promises is a nice way of setting up asynchronous requests.
Then corresponding state config
.state('tabs', {
url: "/tabs",
abstract: true,
templateUrl: "tabs.html"
})
.state('tabs.news', {
url: "/news",
views: {
'tab-news': {
templateUrl: "tab-news.html",
controller: 'NewsCtrl'
}
},
resolve: {
posts: function (FreshlyPressed) {
return FreshlyPressed.getBlogs();
}
}
})
.state('tabs.post-detail', {
url: '/news/:postId',
views: {
'tab-news': {
templateUrl: 'tab-post-detail.html',
controller: 'PostDetailCtrl'
}
},
resolve: {
posts: function (FreshlyPressed) {
return FreshlyPressed.getBlogs();
}
}
})
This will invoke the getBlogs function in your service and wait for the promise to be resolved. After that, you can inject the resolved posts variable into your controllers:
and finally controllers
.controller('NewsCtrl', function($scope, $ionicLoading, FreshlyPressed, posts) {
$scope.posts = posts;
$scope.doRefresh = function() {
FreshlyPressed.getBlogs()
.then(function (posts) {
$scope.posts = posts;
});
}
});
Here we can set up the $scope.posts variable directly using the resolved posts variable. The doRefresh function needs then to be modified so that it will invoke the service function and after the promise is resolved, set the $scope.posts data accordingly.
.controller('PostDetailCtrl', function($scope, $stateParams, FreshlyPressed, posts) {
$scope.post = FreshlyPressed.get($stateParams.postId, posts);
});
And here we give the service get function the resolved posts variable as parameter, as in this case the service isn't caching that data.
This isn't problem-free approach, for example consider following scenario: you get one set of posts data into your main controller. Then, there are new posts posted into the wordpress you are querying, before your user clicks on a title. This might cause the PostDetailCtrl to receive a new set of posts data, that doesn't any longer contain the certain post that your user clicked.
I think this is anyway a viable alternative, and at least a bit of food for thought.

Dynamically Creating Routes in Angular JS

We're trying to make the switch to angular, but we have a pretty big issue with routing. Our current site has something like 10,000 unique routes -- ever page has a unique ".html" identifier. There's no particular convention that would allow us to assign the controller to them, so I created a lookup API endpoint.
Here's the workflow I'm trying to create:
Angular app loads. One "otherwise" route is set up.
When someone clicks a link, I don't know if the resource is a product or a category, so a query is made to the lookup endpoint with the unique ".html" identifier. The endpoint returns two things: the name of the resource, and an ID ("product" and "10" for example). So to be clear, they hit a page like, "http://www.example.com/some-identifier.html," I query the lookup API to find out what kind of resource this is, and get a result like, "product" and "10" -- now I know it's the product controller/template and I need the data from product id 10.
The app assigns the controller and template ("productController" and "product.html"), queries the correct endpoint for data ("/api/product/10"), and renders the template.
The problems I'm facing:
$http isn't available during config, so I can't hit the lookup table.
Adding routes after the config is sloppy at best -- I've done it successfully by assigning $routeProvider to a global variable and doing it after the fact, but man, it's ugly.
Loading all the routes seems impractical -- just the size of the file would be pretty heavy for a lot of connections/browsers.
We can't change the convention now. We have 4 years of SEO and a lot of organic traffic to abandon our URLs.
I feel like I might be thinking about this the wrong way and there's something missing. The lookup table is really the problem -- not knowing what kind of resource to load (product, category, etc). I read this article about loading routes dynamically, but again, he's not making an external query. For us, loading the controllers isn't the problem, it's resolving the routes and then assigning them c
How would you solve the problem?
Solution
Huge thanks to #user2943490 for pointing me in the right direction. Don't forget to upvote his answer! I made it a little more general so that I don't have to define the route types.
API Structure
This configuration requires at least two endpoints: /api/routes/lookup/:resource_to_lookup:/ and /api/some_resource_type/id/:some_resource_id:/. We query the lookup to find out what kind of resource it points to and what the ID of the resource is. This allows you to have nice clean urls, like, "http://www.example.com/thriller.html" (a single) and "http://www.example.com/michaeljackson.html" (a collection).
In my case, if I query something like, "awesome_sweatshirt.html" my lookup will return a JSON object with "{type: 'product', id: 10}". Then I query "/api/product/id/10" to get the data.
"Isn't that slow?" you ask. With varnish in front, all of this happens in way less than 1 second. We're seeing pageload times locally of less than 20ms. Across the wire from a slow dev server was closer to half a second.
app.js
var app = angular.module('myApp', [
'ngRoute'
])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.otherwise({
controller: function($scope, $routeParams, $controller, lookupService) {
/* this creates a child controller which, if served as it is, should accomplish your goal behaving as the actual controller (params.dashboardName + "Controller") */
if ( typeof lookupService.controller == "undefined" )
return;
$controller(lookupService.controller, {$scope:$scope});
delete lookupService.controller;
//We have to delete it so that it doesn't try to load again before the next lookup is complete.
},
template: '<div ng-include="templateUrl"></div>'
});
$locationProvider.html5Mode(true);
})
.controller('appController', ['$scope', '$window', '$rootScope', 'lookupService', '$location', '$route', function($scope, $window, $rootScope, lookupService, $location, $route){
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
window.scrollTo(0,0)
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl($location.path())) {
return;
}
// Show the page load spinner
$scope.isLoaded = false
lookupService.query($location.path())
.then(function (lookupDefinition) {
$route.reload();
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
}]);
lookupService.js
myApp.factory('lookupService', ['$http', '$q', '$location', function lookupService($http, $q, $location) {
return {
id: null,
originalPath: '',
contoller: '',
templateUrl: '',
query: function (url) {
var deferred = $q.defer();
var self = this;
$http.get("/api/routes/lookup"+url)
.success(function(data, status, headers, config){
self.id = data.id;
self.originalPath = url;
self.controller = data.controller+'Controller';
self.templateUrl = '/js/angular/components/'+data.controller+'/'+data.controller+'.html';
//Our naming convention works as "components/product/product.html" for templates
deferred.resolve(data);
})
return deferred.promise;
}
}
}]);
productController.js
myApp.controller('productController', ['$scope', 'productService', 'cartService', '$location', 'lookupService', function ($scope, productService, cartService, $location, lookupService) {
$scope.cart = cartService
// ** This is important! ** //
$scope.templateUrl = lookupService.templateUrl
productService.getProduct(lookupService.id).then(function(data){
$scope.data = data
$scope.data.selectedItem = {}
$scope.$emit('viewLoaded')
});
$scope.addToCart = function(item) {
$scope.cart.addProduct(angular.copy(item))
$scope.$emit('toggleCart')
}
}]);
Try something like this.
In the route config you set up a definition for each resource type and their controllers, templates and a resolve:
$routeProvider.when('/products', {
controller: 'productController',
templateUrl: 'product.html',
resolve: {
product: function ($route, productService) {
var productId = $route.current.params.id;
// productService makes a request to //api/product/<productId>
return productService.getProduct(productId);
}
}
});
// $routeProvider.when(...
// add route definitions for your other resource types
Then you listen for $locationChangeStart. If the URL being navigated to is a "unique identifer", query the lookup. Depending on the resource type returned by the lookup, navigate to the correct route as defined above.
$rootScope.$on('$locationChangeStart', handleUniqueIdentifiers);
function handleUniqueIdentifiers (event, currentUrl, previousUrl) {
// Only intercept those URLs which are "unique identifiers".
if (!isUniqueIdentifierUrl(currentUrl)) {
return;
}
// Stop the default navigation.
// Now you are in control of where to navigate to.
event.preventDefault();
lookupService.query(currentUrl)
.then(function (lookupDefinition) {
switch (lookupDefinition.type) {
case 'product':
$location.url('/products');
break;
case 'category':
$location.url('/categories');
break;
// case ...
// add other resource types
}
$location.search({
// Set the resource's ID in the query string, so
// it can be retrieved by the route resolver.
id: lookupDefinition.id
});
})
.catch(function () {
// Handle the look up error.
});
}
function isUniqueIdentifierUrl (url) {
// Is this a unique identifier URL?
// Right now any url with a '.html' is considered one, substitute this
// with your actual business logic.
return url.indexOf('.html') > -1;
}
You can use $routeParams for this.
e.g.
route/:type/:id
so type and id can be totally dynamic, the different type handling will be up to the route's controller.
What if you have a json file with the information of the routes (and if there is not a security issue) and iterate over it to attach routes to the app?
e.g.
JSON:
routes: [
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
},
{
controller: "Controller1"
path: "/path1"
templateUrl: 'partials/home/home.html'
}
]
And then iterate over the contents of the JSON and attach them to $routeProvider.when ?
I am not sure if it is a good idea, depends how big would be the JSON file and if you dont want to expose all your routes to a possible attacker.
From the AngularJS documentation,
The $routeParams service allows you to retrieve the current set of
route parameters.
Dependencies: $route
Example look like
// Given:
// URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
// Route: /Chapter/:chapterId/Section/:sectionId
// Then
$routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}

Angularjs ng-controller with resolve

I've ran into problem with ng-controller and 'resolve' functionality:
I have a controller that requires some dependency to be resolved before running, it works fine when I define it via ng-route:
Controller code looks like this:
angular.module('myApp')
.controller('MyController', ['$scope', 'data', function ($scope, data) {
$scope.data = data;
}
]
);
Routing:
...
.when('/someUrl', {
templateUrl : 'some.html',
controller : 'MyController',
resolve : {
data: ['Service', function (Service) {
return Service.getData();
}]
}
})
...
when I go to /someUrl, everything works.
But I need to use this controller in other way(I need both ways in different places):
<div ng-controller="MyController">*some html here*</div>
And, of course, it fails, because 'data' dependency wasn't resolved. Is there any way to inject dependency into controller when I use 'ng-controller' or I should give up and load data inside controller?
In the below, for the route resolve, we're resolving the promise and wrapping the return data in an object with a property. We then duplicate this structure in the wrapper service ('dataService') that we use for the ng-controller form.
The wrapper service also resolves the promise but does so internally, and updates a property on the object we've already returned to be consumed by the controller.
In the controller, you could probably put a watcher on this property if you wanted to delay some additional behaviours until after everything was resolved and the data was available.
Alternatively, I've demonstrated using a controller that 'wraps' another controller; once the promise from Service is resolved, it then passes its own $scope on to the wrapped controller as well as the now-resolved data from Service.
Note that I've used $timeout to provide a 1000ms delay on the promise return, to try and make it a little more clear what's happening and when.
angular.module('myApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider
.when('/', {
template: '<h1>{{title}}</h1><p>{{blurb}}</p><div ng-controller="ResolveController">Using ng-controller: <strong>{{data.data}}</strong></div>',
controller: 'HomeController'
})
.when('/byResolve', {
template: '<h1>{{title}}</h1><p>{{blurb}}</p><p>Resolved: <strong>{{data.data}}</strong></p>',
controller: "ResolveController",
resolve: {
dataService: ['Service',
function(Service) {
// Here getData() returns a promise, so we can use .then.
// I'm wrapping the result in an object with property 'data', so we're returning an object
// which can be referenced, rather than a string which would only be by value.
// This mirrors what we return from dataService (which wraps Service), making it interchangeable.
return Service.getData().then(function(result) {
return {
data: result
};
});
}
]
}
})
.when('/byWrapperController', {
template: '<h1>Wrapped: {{title}}</h1><p>{{blurb}}</p><div ng-controller="WrapperController">Resolving and passing to a wrapper controller: <strong>{{data.data ? data.data : "Loading..."}}</strong></div>',
controller: 'WrapperController'
});
})
.controller('HomeController', function($scope) {
$scope.title = "ng-controller";
$scope.blurb = "Click 'By Resolve' above to trigger the next route and resolve.";
})
.controller('ResolveController', ['$scope', 'dataService',
function($scope, dataService) {
$scope.title = "Router and resolve";
$scope.blurb = "Click 'By ng-controller' above to trigger the original route and test ng-controller and the wrapper service, 'dataService'.";
$scope.data = dataService;
}
])
.controller('WrapperController', ['$scope', '$controller', 'Service',
function($scope, $controller, Service) {
$scope.title = "Resolving..."; //this controller could of course not show anything until after the resolve, but demo purposes...
Service.getData().then(function(result) {
$controller('ResolveController', {
$scope: $scope, //passing the same scope on through
dataService: {
data: result
}
});
});
}
])
.service('Service', ['$timeout',
function($timeout) {
return {
getData: function() {
//return a test promise
return $timeout(function() {
return "Data from Service!";
}, 1000);
}
};
}
])
// our wrapper service, that will resolve the promise internally and update a property on an object we can return (by reference)
.service('dataService', function(Service) {
// creating a return object with a data property, matching the structure we return from the router resolve
var _result = {
data: null
};
Service.getData().then(function(result) {
_result.data = result;
return result;
});
return _result;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.27/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.27/angular-route.min.js"></script>
<div ng-app="myApp">
By ng-controller |
By Resolve |
By Wrapper Controller
<div ng-view />
</div>
Create a new module inside which you have the service to inject like seen below.
var module = angular.module('myservice', []);
module.service('userService', function(Service){
return Service.getData();
});
Inject newly created service module inside your app module
angular.module('myApp')
.controller('MyController', ['$scope', 'myservice', function ($scope, myservice) {
$scope.data = data;
// now you can use new dependent service anywhere here.
}
]
);
You can use the mechanism of the prototype.
.when('/someUrl', {
template : '<div ng-controller="MyController" ng-template="some.html"></div>',
controller: function (data) {
var pr = this;
pr.data = data;
},
controllerAs: 'pr',
resolve : {
data: ['Service', function (Service) {
return Service.getData();
}]
}
})
angular.module('myApp')
.controller('MyController', ['$scope', function ($scope) {
$scope.data = $scope.pr.data; //magic
}
]
);
Now wherever you want to use
'<div ng-controller="MyController"></div>'
you need to ensure that there pr.data in the Scope of the calling controller. As an example uib-modal
var modalInstance = $modal.open({
animation: true,
templateUrl: 'modal.html',
resolve: {
data: ['Service', function (Service) {
return Service.getData();
}]
},
controller: function ($scope, $modalInstance, data) {
var pr = this;
pr.data = data;
pr.ok = function () {
$modalInstance.close();
};
},
controllerAs:'pr',
size:'sm'
});
modal.html
<script type="text/ng-template" id="modal.html">
<div class="modal-body">
<div ng-include="some.html" ng-controller="MyController"></div>
</div>
<div class="modal-footer">
<button class="btn btn-primary pull-right" type="button" ng-click="pr.ok()">{{ 'ok' | capitalize:'first'}}</button>
</div>
</script>
And now you can use $scope.data = $scope.pr.data; in MyController
pr.data is my style. You can rewrite the code without PR.
the basic principle of working with ng-controller described in this video https://egghead.io/lessons/angularjs-the-dot
Presuming that Service.getData() returns a promise, MyController can inject that Service as well. The issue is that you want to delay running the controller until the promise resolves. While the router does this for you, using the controller directly means that you have to build that logic.
angular.module('myApp')
.controller('MyController', ['$scope', 'Service', function ($scope, Service) {
$scope.data = {}; // default values for data
Service.getData().then(function(data){
// data is now resolved... do stuff with it
$scope.data = data;
});
}]
);
Now this works great when using the controller directly, but in your routing example, where you want to delay rendering a page until data is resolved, you are going to end up making two calls to Service.getData(). There are a few ways to work around this issue, like having Service.getData() return the same promise for all caller, or something like this might work to avoid the second call entirely:
angular.module('myApp')
.controller('MyController', ['$scope', '$q', 'Service', function ($scope, $q, Service) {
var dataPromise,
// data might be provided from router as an optional, forth param
maybeData = arguments[3]; // have not tried this before
$scope.data = {}; //default values
// if maybeData is available, convert it to a promise, if not,
// get a promise for fetching the data
dataPromise = !!maybeData?$q.when(maybeData):Service.getData();
dataPromise.then(function(data){
// data is now resolved... do stuff with it
$scope.data = data;
});
}]
);
I was trying to solve the problem using ng-init but came across the following warnings on angularjs.org
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
So I started searching for something like ng-resolve and came across the following thread:
https://github.com/angular/angular.js/issues/2092
The above link consists of a demo fiddle that have ng-resolve like functionality. I think ng-resolve can become a feature in the future versions of angular 1.x. For now we can work around with the directive mentioned in the above link.
'data' from route resolve will not be available for injection to a controller activated other than route provider. it will be available only to the view configured in the route provider.
if you want the data to the controller activated directly other than routeprovider activation, you need to put a hack for it.
see if this link helps for it:
http://www.johnpapa.net/route-resolve-and-controller-activate-in-angularjs/
Getting data in "resolve" attribute is the functionality of route (routeProvider) , not the functionality of controller.
Key( is your case : 'data') in resolve attribute is injected as service.
That's why we are able fetch data from that service.
But to use same controller in different place , you have fetch data in controller.
Try this
Service:
(function() {
var myService = function($http) {
var getData = function() {
//return your result
};
return {
getData:getData
};
};
var myApp = angular.module("myApp");
myApp.factory("myService", myService);
}());
Controller:
(function () {
var myApp = angular.module("myApp");
myApp.controller('MyController', [
'$scope', 'myService', function($scope, myService) {
$scope.data = myService.getData();
}
]);
//Routing
.when('/someUrl', {
templateUrl : 'some.html',
controller : 'MyController',
resolve : {
data: $scope.data,
}
})
}());

Resolving data in Angular Controller

I'm pretty new to Angular and I've been going round in circles on this one for a while now.
A bit of background first, I'm using the MEAN stack from mean.io which uses Angular UI Router.
I have a Post model in my DB which can have a category id assigned to it.
When I create a new post I want to load the existing categories from the DB and display them in a select box.
From what I can see I need to use resolve, first of all it doesn't feel right having logic in the resolve property which is in a file called config.js - so far I've placed the call to a service in there and im getting the categories back using the following code:
.state('create post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
resolve: {
loadCategories: function (Categories) {
Categories.query(function(categories) {
return categories;
});
}
}
})
The first problem is that I can't access the returned data in my controller or view.
Secondly I only want to load Categories that belong to a certain Organisation. I will be assigning an organisation id to each user so how can I access the currently signed in user when I'm in config.js - again this doesn't feel like the right place to be doing this sort of logic though.
Any help would be really appreciated.
Thanks
config.js:
register post state :
.state('post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
resolve: PostsController.resolve
})
register posts controller:
.controller({
PostsController: ['$scope', 'loadCategories', PostsController],
...
})
controller function:
function PostsController($scope, loadCategories){
$scope.categories = loadCategories;
};
PostsController.resolve = {
loadCategories: ['dependencies', function(dependencies){
return dependencies.query(...)
}]
};
Angular manage your dependency injection
Assuming Categories is an angular resource, you should be able to just
loadCategories: function (Categories) {
return Categories.query();
}
And then in your controller:
.controller('PostsController', function ($scope, loadCategories) {
$scope.categories = loadCategories;
});
Ok, reading your comments, it sounds like you'll have some issue because you want to inject this into the controller, but only in certain states. You could try:
.state('create post', {
url: '/posts/create',
templateUrl: 'views/posts/create.html',
controller: 'PostsController',
data: {
categories: Categories.query()
}
})
and then
.controller('PostsController', function ($scope, $state){
console.log($state.current.data.categories);
});
Which should work...

Delaying AngularJS route change until model loaded to prevent flicker

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetched using its respective services.
For example, if there were a ProjectsController that listed all Projects and project_index.html which was the template that showed these Projects, Project.query() would be fetched completely before showing the new page.
Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).
$routeProvider resolve property allows delaying of route change until data is loaded.
First define a route with resolve attribute like this.
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: PhoneListCtrl.resolve}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
notice that the resolve property is defined on route.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function(Phone, $q) {
// see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
},
delay: function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
}
Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phones is injected into the controller and it is defined in the resolve property.
The resolve.phones function is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.
Working demo: http://mhevery.github.com/angular-phonecat/app/#/phones
Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831
Here's a minimal working example which works for Angular 1.0.2
Template:
<script type="text/ng-template" id="/editor-tpl.html">
Editor Template {{datasets}}
</script>
<div ng-view>
</div>
JavaScript:
function MyCtrl($scope, datasets) {
$scope.datasets = datasets;
}
MyCtrl.resolve = {
datasets : function($q, $http) {
var deferred = $q.defer();
$http({method: 'GET', url: '/someUrl'})
.success(function(data) {
deferred.resolve(data)
})
.error(function(data){
//actually you'd want deffered.reject(data) here
//but to show what would happen on success..
deferred.resolve("error value");
});
return deferred.promise;
}
};
var myApp = angular.module('myApp', [], function($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/editor-tpl.html',
controller: MyCtrl,
resolve: MyCtrl.resolve
});
});​
​
http://jsfiddle.net/dTJ9N/3/
Streamlined version:
Since $http() already returns a promise (aka deferred), we actually don't need to create our own. So we can simplify MyCtrl. resolve to:
MyCtrl.resolve = {
datasets : function($http) {
return $http({
method: 'GET',
url: 'http://fiddle.jshell.net/'
});
}
};
The result of $http() contains data, status, headers and config objects, so we need to change the body of MyCtrl to:
$scope.datasets = datasets.data;
http://jsfiddle.net/dTJ9N/5/
I see some people asking how to do this using the angular.controller method with minification friendly dependency injection. Since I just got this working I felt obliged to come back and help. Here's my solution (adopted from the original question and Misko's answer):
angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: PhoneListCtrl,
resolve: {
phones: ["Phone", "$q", function(Phone, $q) {
var deferred = $q.defer();
Phone.query(function(successData) {
deferred.resolve(successData);
}, function(errorData) {
deferred.reject(); // you could optionally pass error data here
});
return deferred.promise;
]
},
delay: ["$q","$defer", function($q, $defer) {
var delay = $q.defer();
$defer(delay.resolve, 1000);
return delay.promise;
}
]
},
}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: PhoneDetailCtrl,
resolve: PhoneDetailCtrl.resolve}).
otherwise({redirectTo: '/phones'});
}]);
angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}]);
Since this code is derived from the question/most popular answer it is untested, but it should send you in the right direction if you already understand how to make minification friendly angular code. The one part that my own code didn't requires was an injection of "Phone" into the resolve function for 'phones', nor did I use any 'delay' object at all.
I also recommend this youtube video http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , which helped me quite a bit
Should it interest you I've decided to also paste my own code (Written in coffeescript) so you can see how I got it working.
FYI, in advance I use a generic controller that helps me do CRUD on several models:
appModule.config ['$routeProvider', ($routeProvider) ->
genericControllers = ["boards","teachers","classrooms","students"]
for controllerName in genericControllers
$routeProvider
.when "/#{controllerName}/",
action: 'confirmLogin'
controller: 'GenericController'
controllerName: controllerName
templateUrl: "/static/templates/#{controllerName}.html"
resolve:
items : ["$q", "$route", "$http", ($q, $route, $http) ->
deferred = $q.defer()
controllerName = $route.current.controllerName
$http(
method: "GET"
url: "/api/#{controllerName}/"
)
.success (response) ->
deferred.resolve(response.payload)
.error (response) ->
deferred.reject(response.message)
return deferred.promise
]
$routeProvider
.otherwise
redirectTo: '/'
action: 'checkStatus'
]
appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->
$scope.items = items
#etc ....
]
This commit, which is part of version 1.1.5 and above, exposes the $promise object of $resource. Versions of ngResource including this commit allow resolving resources like this:
$routeProvider
resolve: {
data: function(Resource) {
return Resource.get().$promise;
}
}
controller
app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {
$scope.data = data;
}]);
This snippet is dependency injection friendly (I even use it in combination of ngmin and uglify) and it's a more elegant domain driven based solution.
The example below registers a Phone resource and a constant phoneRoutes, which contains all your routing information for that (phone) domain. Something I didn't like in the provided answer was the location of the resolve logic -- the main module should not know anything or be bothered about the way the resource arguments are provided to the controller. This way the logic stays in the same domain.
Note: if you're using ngmin (and if you're not: you should) you only have to write the resolve functions with the DI array convention.
angular.module('myApp').factory('Phone',function ($resource) {
return $resource('/api/phone/:id', {id: '#id'});
}).constant('phoneRoutes', {
'/phone': {
templateUrl: 'app/phone/index.tmpl.html',
controller: 'PhoneIndexController'
},
'/phone/create': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
phone: ['$route', 'Phone', function ($route, Phone) {
return new Phone();
}]
}
},
'/phone/edit/:id': {
templateUrl: 'app/phone/edit.tmpl.html',
controller: 'PhoneEditController',
resolve: {
form: ['$route', 'Phone', function ($route, Phone) {
return Phone.get({ id: $route.current.params.id }).$promise;
}]
}
}
});
The next piece is injecting the routing data when the module is in the configure state and applying it to the $routeProvider.
angular.module('myApp').config(function ($routeProvider,
phoneRoutes,
/* ... otherRoutes ... */) {
$routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });
// Loop through all paths provided by the injected route data.
angular.forEach(phoneRoutes, function(routeData, path) {
$routeProvider.when(path, routeData);
});
$routeProvider.otherwise({ redirectTo: '/' });
});
Testing the route configuration with this setup is also pretty easy:
describe('phoneRoutes', function() {
it('should match route configuration', function() {
module('myApp');
// Mock the Phone resource
function PhoneMock() {}
PhoneMock.get = function() { return {}; };
module(function($provide) {
$provide.value('Phone', FormMock);
});
inject(function($route, $location, $rootScope, phoneRoutes) {
angular.forEach(phoneRoutes, function (routeData, path) {
$location.path(path);
$rootScope.$digest();
expect($route.current.templateUrl).toBe(routeData.templateUrl);
expect($route.current.controller).toBe(routeData.controller);
});
});
});
});
You can see it in full glory in my latest (upcoming) experiment.
Although this method works fine for me, I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object; it would make things soooOOOOOooOOOOO much easier.
Edit: used Angular v1.2(rc2)
Delaying showing the route is sure to lead to an asynchronous tangle... why not simply track the loading status of your main entity and use that in the view. For example in your controller you might use both the success and error callbacks on ngResource:
$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
$scope.httpStatus = 200;
}, function(response) {
$scope.httpStatus = response.status;
});
Then in the view you could do whatever:
<div ng-show="httpStatus == 0">
Loading
</div>
<div ng-show="httpStatus == 200">
Real stuff
<div ng-repeat="project in projects">
...
</div>
</div>
<div ng-show="httpStatus >= 400">
Error, not found, etc. Could distinguish 4xx not found from
5xx server error even.
</div>
I worked from Misko's code above and this is what I've done with it. This is a more current solution since $defer has been changed to $timeout. Substituting $timeout however will wait for the timeout period (in Misko's code, 1 second), then return the data hoping it's resolved in time. With this way, it returns asap.
function PhoneListCtrl($scope, phones) {
$scope.phones = phones;
$scope.orderProp = 'age';
}
PhoneListCtrl.resolve = {
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
}
Using AngularJS 1.1.5
Updating the 'phones' function in Justen's answer using AngularJS 1.1.5 syntax.
Original:
phones: function($q, Phone) {
var deferred = $q.defer();
Phone.query(function(phones) {
deferred.resolve(phones);
});
return deferred.promise;
}
Updated:
phones: function(Phone) {
return Phone.query().$promise;
}
Much shorter thanks to the Angular team and contributors. :)
This is also the answer of Maximilian Hoffmann. Apparently that commit made it into 1.1.5.
You can use $routeProvider resolve property to delay route change until data is loaded.
angular.module('app', ['ngRoute']).
config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
$routeProvider.
when('/entities', {
templateUrl: 'entities.html',
controller: 'EntitiesCtrl',
resolve: EntitiesCtrlResolve
}).
when('/entity/:entityId', {
templateUrl: 'entity.html',
controller: 'EntityCtrl',
resolve: EntityCtrlResolve
}).
otherwise({redirectTo: '/entities'});
}]);
Notice that the resolve property is defined on route.
EntitiesCtrlResolve and EntityCtrlResolve is constant objects defined in same file as EntitiesCtrl and EntityCtrl controllers.
// EntitiesCtrl.js
angular.module('app').constant('EntitiesCtrlResolve', {
Entities: function(EntitiesService) {
return EntitiesService.getAll();
}
});
angular.module('app').controller('EntitiesCtrl', function(Entities) {
$scope.entities = Entities;
// some code..
});
// EntityCtrl.js
angular.module('app').constant('EntityCtrlResolve', {
Entity: function($route, EntitiesService) {
return EntitiesService.getById($route.current.params.projectId);
}
});
angular.module('app').controller('EntityCtrl', function(Entity) {
$scope.entity = Entity;
// some code..
});
I like darkporter's idea because it will be easy for a dev team new to AngularJS to understand and worked straight away.
I created this adaptation which uses 2 divs, one for loader bar and another for actual content displayed after data is loaded. Error handling would be done elsewhere.
Add a 'ready' flag to $scope:
$http({method: 'GET', url: '...'}).
success(function(data, status, headers, config) {
$scope.dataForView = data;
$scope.ready = true; // <-- set true after loaded
})
});
In html view:
<div ng-show="!ready">
<!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
<div class="progress progress-striped active">
<div class="bar" style="width: 100%;"></div>
</div>
</div>
<div ng-show="ready">
<!-- Real content goes here and will appear after loading -->
</div>
See also: Boostrap progress bar docs
I liked above answers and learned a lot from them but there is something that is missing in most of the above answers.
I was stuck in a similar scenario where I was resolving url with some data that is fetched in the first request from the server. Problem I faced was what if the promise is rejected.
I was using a custom provider which used to return a Promise which was resolved by the resolve of $routeProvider at the time of config phase.
What I want to stress here is the concept of when it does something like this.
It sees the url in url bar and then respective when block in called controller and view is referred so far so good.
Lets say I have following config phase code.
App.when('/', {
templateUrl: '/assets/campaigns/index.html',
controller: 'CampaignListCtr',
resolve : {
Auth : function(){
return AuthServiceProvider.auth('campaign');
}
}
})
// Default route
.otherwise({
redirectTo: '/segments'
});
On root url in browser first block of run get called otherwise otherwise gets called.
Let's imagine a scenario I hit rootUrl in address bar AuthServicePrivider.auth() function gets called.
Lets say Promise returned is in reject state what then???
Nothing gets rendered at all.
Otherwise block will not get executed as it is for any url which is not defined in the config block and is unknown to angularJs config phase.
We will have to handle the event that gets fired when this promise is not resolved. On failure $routeChangeErorr gets fired on $rootScope.
It can be captured as shown in code below.
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// Use params in redirection logic.
// event is the routeChangeEvent
// current is the current url
// previous is the previous url
$location.path($rootScope.rootPath);
});
IMO It's generally a good idea to put event tracking code in run block of application. This code run just after the config phase of the application.
App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
$rootScope.rootPath = "my custom path";
// Event to listen to all the routeChangeErrors raised
// by the resolve in config part of application
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
// I am redirecting to rootPath I have set above.
$location.path($rootScope.rootPath);
});
}]);
This way we can handle promise failure at the time of config phase.
I have had a complex multi-level sliding panel interface, with disabled screen layer. Creating directive on disable screen layer that would create click event to execute the state like
$state.go('account.stream.social.view');
were producing a flicking effect. history.back() instead of it worked ok, however its not always back in history in my case. SO what I find out is that if I simply create attribute href on my disable screen instead of state.go , worked like a charm.
<a class="disable-screen" back></a>
Directive 'back'
app.directive('back', [ '$rootScope', function($rootScope) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
}
};
} ]);
app.js I just save previous state
app.run(function($rootScope, $state) {
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
$rootScope.previousState = fromState.name;
$rootScope.currentState = toState.name;
});
});
One possible solution might be to use the ng-cloak directive with the element where we are using the models e.g.
<div ng-cloak="">
Value in myModel is: {{myModel}}
</div>
I think this one takes least effort.

Categories

Resources