Routes loaded from an api - controller never called - javascript

I'm trying to load routes into my angularJS app by running an ajax call and setting up routes on my RouteProvidor. The following is my code to do so.
var app = angular.module('app', ['ngRoute']);
var appRouteProvider;
app.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
appRouteProvider = $routeProvider;
appRouteProvider.when('/', {
templateUrl : '../../app/templates/home.html',
controller : 'IndexController'
});
}]).run(function($http, $route){
url = siteApiRoot+'api/routes';
$http.post(url, jQuery.param([]), {
method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}
).success(function(data){
for(i in data){
appRouteProvider.when(data[i].route, {
templateUrl : data[i].template,
controller : data[i].controller
});
}
console.log($route.routes);
});
});
The console.log outputs a correct set of routes to the console which seems to indicate that the routes have been correctly assigned. But if I were to open any url that should be handled by the route. Nothing happens. The basic assets load i.e. navigation bar and footer which are constant throughout but the controller for the route is never called. What am I missing here guys.
-- UPDATE
Tehcnically I have routes that follow the following patterns:
List of entries:
/<city>/<category>
/<city>/<subdistrict>-<category>
/<city>/<entry-slug>
I'm not sure how well to define the above - basically the first two routes would invoke one controller and one view while the third woudl invoke another. However I'm stuck with how to define this kind of routing in AngularJS provided that etc are all slugs in a database. Pretty much left with hardcoding an array of routes but that also doesn't work as it seems.
Plus I also have other pages that are static - eg /about /site/contact - a bit lost on routing here.

You can't change the router configuration after initialisation, but you can use a parameterized route to handle everything.
You can fetch the routing data in an external service, and find the appropiate entry for the current parameters with whatever lookup logic you need. I assume the point of this is to have different templates and controllers for these routes.
The template you can solve with a simple ng-include, but you'll have to manually instantiate the controller. Look into $injector instead of the $controller call here for more details on this one, as you'll probably need full dependency injection for them. The RouteController here just passes its own scope to the created controller (which at this point really is just like any generic service), which is already attached to the container.html by the router. Note that the ng-include creates a child scope, so you have to be careful if you want to assign new variables on the scope in templates.
(If this is a problem, you can manually fetch, build and attach the template too: take a look into $templateRequest, $templateCache and $compile services. (You will have to create a directive to attach it to the DOM))
Here is the barebones sample code:
var app = angular.module('app', ['ngRoute']);
app.service("getRouteConfig", function($http) {
var routeRequest = $http.post(url, jQuery.param([]), {
method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}
return function(params) {
return routeRequest.then(function(routes) {
// find route entry in backend data for current city, category/slug
return routes[params.city][params.slug];
});
}
})
app.controller("RouteController", function(route, $scope, $controller) {
$controller(route.controller, {$scope: $scope});
})
app.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl : '../../app/templates/home.html',
controller : 'IndexController'
});
$routeProvider.when('/:city/:url', {
templateUrl : '../../app/templates/container.html',
controller : 'RouteController',
resolve: {
route: function(getRouteConfig, $routeParams) {
return getRouteConfig($routeParams);
}
}
});
});
container.html:
<ng-include src='$resolve.route.template'>

Angular config functions are for setting up configuration, which is used for initialising services. This means that trying to alter the config from the run() function will result in nothing happening, as the config has already been utilised.
One possible option is to provide the config from the server inside the actual js file sent to the client. Otherwise there is no easy way to alter config using $http.
There is more discussion here: use $http inside custom provider in app config, angular.js

Related

Using $http or $timeout with $stateProvider in AngularJS

I come here from this question Is it possible to load a template via AJAX request for UI-Router in Angular?
I wish to load a template for a given state using $http.
ui.router docs shows examples where both $timeout and $http are used within $stateProvider config, e.g.
Or you can use a template provider function which can be injected, has
access to locals, and must return template HTML, like this:
$stateProvider.state('contacts', { templateProvider: function
($timeout, $stateParams) {
return $timeout(function () {
return '<h1>' + $stateParams.contactId + '</h1>'
}, 100);
}
})
However, as I understand it, $stateProvider has to be configured during module.config phase, and $timeout is not available then. I cannot find any example which shows how to access $timeout (or $http) for $stateProvider exactly per the example provided by the docs.
Is there a way to access either $timeout or $http as defined in the docs? Do I configure state at another point? Is there an alternative way to do this which I'm overlooking?
The templateProvider function itself is injected with the dependencies using injector service, and it has nothing to do with the config phase.
So you should be able to use $http simply by injecting it in the function. If you see the example, $timeout or $stateParams are also not available at the config phase, but they work fine in the templateProvider function.
For example, the below snippet returns the content of HTTPBin:
templateProvider: function($http) {
return $http.get('https://httpbin.org/get')
.then(function(response) {
return JSON.stringify(response.data);
});
}
For more details, refer to this fiddle.

Dynamically loading Views and Controllers in AngularJS

I want to know what's that proper way to handle the business logic in terms of loading the appropriate views and controllers.
I have a bunch of factories to load resources, read and write user progress (to a local file) and so forth. The user will spend no more than several seconds on a given view (there are 6-7 different ones in total) and will switch to another one with dynamically loaded resources based on his progress.
My current idea is to have a service/factory that keeps track of progress, which is loaded on the index page and then every controller sends a request to it, once it's finished. Thereafter the service changes the $state and loads the appropriate data.
I'm building my first app in AngularJS and I have tried to search StackOverflow and Google, but I still can't figure out how to approach this problem.
Even pointing me to the right direction or reading material would be greatly appreciated.
If you are creating AngularJs app first time then follow simple steps.
1. Create one index.JSP file where you should run your app using ng-app directive and add all scripts and files.
2. create one Js file app.js.
add all module name in app.js and run your js using .run method.
3. maintain services, controllers and filters, directives, templates seperately in different folders and in different files.
And dont forget to add module name in app.js and add path in index.jsp
4. In your services files write only sharing business logic.
All other business logic related to particular file write it in controller.
Here your are maintaining ajax calls so dont meshup it with controllers.
.Service
.factory('angularService', function () {
return {
// Write business logic
}
})
Declare $starteProvider and define .states in your controller.
For ex.
$stateProvider.state('xyz_state', function () {
// add url,
// templateUrl,
// controller
})
.controller('myFirstController', function () {
// Add your business logic
// scope variables
});
6. Maintain view pages separately.
7. Maintain Directives and Filters separately.
In angular views are by default loaded through ajax call when it is required. If you want to do the same in controller then use require.js. which will load the controller dynamically when it is required. In require.js you can also specify your other dependency library for every view & controller which will loaded through ajax when view is called.
index.html
<script data-main="js/main.js" type="text/javascript" src="js/require.js"></script>
main.js
require.config({
urlArgs: 'v=1.0',
});
require(
[
'app'
],
function() {
angular.bootstrap(document, ['UConnect']); //Add your module
}
);
app.js
'use strict';
define([],function() {
var app = angular.module('UConnect',['ngRoute','ngAnimate','ui.materialize','yaru22.angular-timeago']);
app.config(['$routeProvider', '$controllerProvider', '$provide',function($routeProvider, $controllerProvider, $provide) {
// Register your component
app.register = {
controller: $controllerProvider.register,
factory: $provide.factory,
service: $provide.service
};
// Add resolver for load controller through require.js
function resolveController(dependencies) {
return {
load: ['$q', '$rootScope', function ($q, $rootScope) {
var defer = $q.defer();
require(dependencies, function () {
defer.resolve();
$rootScope.$apply();
});
return defer.promise;
}]
}
};
$routeProvider
.when("/Pages", {
templateUrl : "templates/Pages.html",
controller: 'PagesCtrl', // Add controller name.
resolve: resolveController(['controller/PagesCtrl']) // Call resolver to load controller.
})
.when("/ContactUs", {
templateUrl : "templates/ContactUs.html",
controller: 'ContactUsCtrl',
resolve: resolveController(['controller/ContactUsCtrl'])
})
;
$routeProvider.otherwise('/Pages');
}]);
angular.element(document).ready(function () {
angular.bootstrap(document, ['Uconnect']);
});
return app;
});
I usually use the $templateCache & store all my views there. if your on Node environment i'd suggest add ng-html2js to your build process.
as for controllers & other JS you should minify and concat all and load just that file in the beginning.

I have all my templates in $templateCache, but Angular is making requests anyway

I'm using gulp-angular-templatecache to put all my .html files in the $templateCache when deploying my app.
The problem is that Angular (and ui-router) are ignoring this and sending requests to the server, even though my routes' names are exactly the same names that $templateCache is using to store the files.
Example of one state:
.state('app', {
url: '/app',
templateUrl: '/base/view/app.html',
controller: 'AppController as appCtrl'
});
And $templateCache:
$templateCache.put('/base/view/app.html', '<div>...</div>');
This won't work. It's still requesting the app.html.
However, if I remove templateUrl and use $templateProvider instead, it will work. Like this:
.state('app', {
url: '/app',
templateProvider: ['$templateCache', function($templateCache) {
if ($templateCache.get('/base/view/app.html')) {
return $templateCache.get('/base/view/app.html');
}
}],
controller: 'AppController as appCtrl'
});
But there are tons of states and directives and I would need to manually replace them all. So I created an interceptor of http requests to check if it's requesting an .html, if it is, then check if it's cached and return it, otherwise, make the request.
However, I'm unable to stop the request and return what I want. I can only stop it but I can't put a 'fake' response on it.
What I have tried:
'request': function(config) {
var url = angular.copy(config.url);
if (url.indexOf('.html') !== -1) {
var cachedData = $templateCache.get(url);
if (!_.isUndefined(cachedData)) {
var cachedPromise = $q.defer();
config.timeout = cachedPromise.promise;
cachedPromise.resolve();
// config.timeout is used to stop the request but it doesn't matter the response of the promise, it's probably ignored and just used to stop the request.
}
}
return config;
}
Is there any way to make this interceptor work? Another idea would be to alter the ui-router source code to make it check $templateCache beforing sending the request (It looks like it does check the cache, but only after the first request. I want to check it even before the first request). Besides that, I'm out of ideas... Any way to make this work without changing the source code of ui-router?
I had too a similar problem, and MMhunter's comment has saved me!
Then that is wired. One possible cause of this issue I can think of is
that you put your template cache initialization and the route config
in two separate modules with no dependency to each other. This could
somehow make then do not share the same $templateCache service.
gulp-angular-templatecache is using or creating (with standalone:true option) a module for template caches. This module must be added as a dependency in root app's module.
So, if you have this cache:
angular
.module('templates', [])
.run(['$templateCache', function ($templateCache) {
$templateCache.put('get-time/get-time.html', '<div style="background-color:darkgray;"><h1>Get Time</h1><p>The time is: {{time}} <img src="/Areas/Admin/assets/img/clock.png" width="100"></p></div>'); }]);
Then you must have a dependency like this:
angular.module("app", ["templates"])
I've found out the problem.
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if (typeof(toState) !== 'undefined') {
$templateCache.remove(toState.templateUrl);
}
});
That is why it was working with the interceptor. The code above was disabling $templateCache, but the interceptor is executed after that. So, remember to check your handlers, interceptors, etc.

Defer controller rendering

I have an AngularJS app which grab data from PHP via AJAX and permit user to edit it through few steps.
Structure of the app is very simple :
I have a main controller which is loaded from ng-controller directive.
<div ng-controller="MainCtrl">
<!-- All my app take place here, -->
<!-- so all my others controllers are in MainCtrl scope -->
<div ng-view></div>
</div>
I have one controller by editing steps (ex. general info, planner, validation, etc.). Each controller is loaded by the $routeProvider (inside MainCtrl scope).
My problem is when I load (or refresh) the page, MainCtrl make an AJAX request to retrieve data to edit. The controller attached to $routeProvider is loaded before AJAX request is finished, so I can't use data grabbed by MainCtrl.
I want to defer $routeProvider loading route while AJAX request is not ended. I think I have to use the $q provider, but I can't prevent route loading.
I have tried this (in MainCtrl) and controller is still rendered premature :
$scope.$on('$routeChangeStart', function(event, current, previous) {
$scope.pathLoaded.promise.then(function() {
// Data loaded => render controller
return true;
});
// Stop controller rendering
return false;
});
And AJAX call is defined like this :
$scope.pathLoaded = $q.defer();
// Edit existing path
$http.get(Routing.generate('innova_path_get_path', {id: EditorApp.pathId}))
.success(function (data) {
$scope.path = data;
$scope.pathLoaded.resolve();
})
.error(function(data, status) {
// TODO
});
So the question is : is it the good way to achieve this ? And if yes, how can I defer controller rendering ?
Thanks for help.
You can use the resolve property of routes, execute the AJAX there and pass the result to your controller. In the route definition:
$routeProvider.when("path", {
controller: ["$scope", "mydata", MyPathCtrl], // NOTE THE NAME: mydata
templateUrl: "...",
resolve: {
mydata: ["$http", function($http) { // NOTE THE NAME: mydata
// $http.get() returns a promise, so it is OK for this usage
return $http.get(...your code...);
}]
// You can also use a service name instead of a function, see docs
},
...
});
See docs for more details. The controller for the given path will not be called before all members in the resolve object are resolved.

AngularJS Restful Routing

I'm trying to structure my app using the Restful/Ruby convension /<resource>/[method]/[id]. How I've done it previously when using a server-side MVC framework like CodeIgniter was to dynamically route based on the URI:
ex.
www.foo.com/bar/baz/1
The app would then use method baz in controller/class bar and return views/bar/baz.php (populated with data from bar->baz)
I would like to do the same in Angular, but I'm not sure if it supports this (and if it does, I'm not sure exactly how to go about it). At the moment I'm using $routeProvider's when method to specify each case. $location.path() looks like it might have what I need, but I don't think I can use it in app.js (within config()).
What I'd like to do is something like this:
.config([
'$routeProvider', function($routeProvider) {
$routeProvider
.when(//<resource> controller exists
resource+'/'+method, {
"templateURL": "views/" + resource + "/" + method + ".html",
"controller": resource
}
).otherwise({ "redirectTo":"/error" });
}
]);
And the router automatically calls the appropriate method.
EDIT Also, why does $routeProvider freak out when I specify when('/foo/bar', {…}) ?
EDIT 2 Per Lee's suggestion, I'm looking into doing something like this:
$routeProvider
.when(
'/:resource/:method/:id', {
"templateUrl": function(routeParams){
var path = 'views/'+routeParams.resource+'/';
return ( typeof routeParams.method === 'undefined' ) ?
path+'index.html' : path+routeParams.method+'.html';
},
"controller": RESOURCE
})
.otherwise({redirectTo: '/error'});
I noticed the following in $routeProvider's doc:
templateUrl – {string=|function()=} – path or function that returns a
path to an html template that should be used by ngView.
If templateUrl is a function, it will be called with the following
parameters:
• {Array.<Object>} - route parameters extracted from the current
$location.path() by applying the current route
Edit: The option to set templateUrl to a function is part of the unstable 1.1.2 build: #1963 (but it doesn't work as of 2013-02-07).
There is a dicussion about adding this functionality on AngularJS's Github: #1193 #1524, but I can't tell if it was actually implemented (in the docs from Dash quoted above, it looks like it has been, and the docs on the site haven't been updated yet).
EDIT 3 To clarify what I want to happen (per lee's request), in simplest terms, I would like to go to www.foo.com/index.html#/people
Angular should use controller people, automatically call its index method, and should serve up
./views/people/index.html
./views/people/map.html
Also, if I go to www.foo.com/index.html#/people/map
Angular should use the people controller again, but this time automcatically call its map method and serve up …map.html (because map was specified in the url)
./views/people/index.html
./views/people/map.html
Then, if I go to
www.foo.com/index.html#/widgets
Angular should serve up
./views/widgets/index.html
./views/widgets/details.html
The code for the router should be very generic—I shouldn't have to specify a .when() for every route.
Thinking about this a little more. You could just have a single controller for those generic CRUD/REST type operations. Then load the templates using the resource and view parameters.
Create
#/foo/create/0
This has it's own form template "/views/foo/create.html" and the 0 os just there for a placeholder.
on submit you would call a method on the controller ng-click="save()" which would post to the server at POST "/rest/foo".
Read
#/foo/view/1
Again the template "/views/foo/view.html" is just a view of the data
You can call a service method to get the data from your server using GET "/rest/foo/1"
Update
-#/foo/edit/1
Could use the same template as create or you could use a different one "/views/foo/edit.html" if you like.
Also pull the data using GET "/rest/foo/1"
Submit the data using PUT "/rest/foo/1"
Delete
#/foo/delete/1
service method would call DELETE "/rest/foo/1"
I don't think you want a hash for this, but you could use one because the controller could actually do a verification or anything you like to confirm the deletion. Maybe have a view called "/views/foo/delete.html" that asks if you want to delete the record. Then you could have ng-click="delete(itemid)" on a button somewhere that deletes the item via ajax.
All this could be done using a single controller/service and dynamically generating the service and view urls.
Anything that's custom you would need a custom controller and custom routes and service methods for. I could probably throw together an example, but not tonight.
Here is a project on github that does something close to what you are asking
EDIT:
I discovered something interesting that had not occurred to me before. If you leave out the controller in the route it will use the controller specified in the template. So as long as all the templates that you use for a given controller have ng-controller="resource" then it will load that controller for the template as expected. Of course with the current implementation of routes there are no optional parameters, so if you have two or three parameters you would need to specify a separate route. Biggest problem is it appears to call the controller method twice. I am guessing this is because there are two views with the same controller. However one view should replace the other so there should not be two calls. This seems like a bug to me. I also found some discussion of a possible new routing system in the works that may meet your needs, but it may be pretty far off: https://github.com/angular-ui/router/issues?page=1&state=open. The sample on github is now using the following method so you can browse that if you like.
var restrouteApp = angular.module('restrouteApp', [])
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/:ctrl/:method', {
templateUrl: function(rp){
if(!rp.method) {rp.method = 'index';}
console.log('route one');
return 'views/'+rp.ctrl+'/'+rp.method+'.html';
}
})
.when('/:ctrl/:method/:id', {
templateUrl: function(rp){
if(!rp.method) {rp.method = 'index';}
console.log('route two');
return 'views/'+rp.ctrl+'/'+rp.method+'.html';
}
})
.otherwise({
redirectTo: '/resource1/'
});
}]);
And the templates:
<div ng-controller="resource1">
<h1> resource1/one.html </h1>
<div>{{r1data.selected}}</div>
</div>
Now in your controller you can do this to call the method dynamically.
restrouteApp.controller('resource1', function($scope,$routeParams,$log,Resource1Service) {
$log.info('new resource1');
$scope.controllername = $routeParams.ctrl;
$scope.r1data= Resource1Service.shared;
$scope.index = function(){
Resource1Service.index().then(function(){
//when the service returns
});
}
$scope.one = function(){
$scope.r1data.selected = $scope.r1data.resources[0];
}
$scope.two= function(){
$scope.r1data.selected = $scope.r1data.resources[1];
}
//call the specified method of this controller
$scope[$routeParams.method]();
});
/EDIT
To conform to existing routing systems like Rails, the ability to define the method in the route is now available.
I created a super simple solution that allows routes to call a method based on the route definition and a directive in the view. I think ui-router is not conventional and is too complicated for a such a "should be" core feature.
The project is called ngMethod and is located at: https://github.com/jzumbrun/ng-method.
An example of its use is: https://github.com/jzumbrun/chrome-apps-angularjs-bootstrap
So if I have a route like so:
$routeProvider.
when('/contacts/new', {
controller: 'ContactsController',
method: 'new',
templateUrl: $configProvider.template('contacts/form.html'),
});
$routeProvider.
when('/contacts/:id/edit', {
controller: 'ContactsController',
method: 'edit',
templateUrl: $configProvider.template('contacts/form.html'),
});
and I have ng-method in the contacts/form template:
<div class="col-lg-12" ng-method>
<form role="form">
...
Then the ng-method will call either $scope.edit() or $scope.new() in the ContactsController.
Than the contacts/form template can be shared, and depending on the route call the correct method
to load the data. This style is now more "Angularjs" and the loading the code is much like angular calling to modules and controllers.
The full directive that makes this happen is less than 20 lines of code:
app.directive('ngMethod', ['$route', function($route) {
return {
// Restrict it to be an attribute in this case
restrict: 'A',
// responsible for registering DOM listeners as well as updating the DOM
link: function(scope, element, attrs) {
// Call method without params. Use $routeParams
if(angular.isFunction(scope[attrs.ngMethod])){
scope[attrs.ngMethod]();
// default to the route method if attrs.ngMethod is empty
} else if(angular.isObject($route.current)
&& angular.isString($route.current['method'])
&& angular.isFunction(scope[$route.current['method']])){
scope[$route.current['method']]();
}
}
};
}]);
This is now possible with ui-router 0.2.8:
$stateProvider
.state('base', {
url: '/:resource/:collection/:id',
controllerProvider: function( $stateParams )
{ // assuming app.controller('FooCtrl',[…])
return $stateParams.collection + 'Ctrl';
},
templateUrl: function( $stateParams )
{
return '/partials/' + $stateParams.collection + '.html';
}
});
But in order to take advantage of $state.includes() on nav menus, this would probably be better:
$stateProvider
.state('base.RESOURCE_NAME1', {
url: '/:collection/:id',
controllerProvider: function( $stateParams )
{ // assuming the convention FooCtrl
return $stateParams.collection + 'Ctrl';
},
templateUrl: function( $stateParams )
{
return '/partials/' + $stateParams.collection + '.html';
}
}).state('base.RESOURCE_NAME2', {
url: '/:collection/:id',
controllerProvider: function( $stateParams )
{ // assuming the convention FooCtrl
return $stateParams.collection + 'Ctrl';
},
templateUrl: function( $stateParams )
{
return '/partials/' + $stateParams.collection + '.html';
}
});
The above could be simplified with a loop to build the states from an array of resources ($stateProvider supports adding states basically whenever):
var resources = [ 'r1', 'r2', '…' ];
for ( var r = resources.length-1; r >=0; r-- )
{
var name = resources[r];
$stateProvider.state('base.'+name, {
…
});
}
Caveat ui-router doesn't not really support optional state parameters (planned for v0.4)

Categories

Resources