I am looking for a Solution to load my App Content dynamically from the Server.
My Scenario:
Lets say we have 2 Users (A and B), my App consists of different Modules like lets say a shoppingList and a calculator, now my goal would be the User logs into my App from the Database I get the User rights and depending what rights he has, i would load the html for the views and the controller files for the logic part from the Server, while doing that I would create the states needed for the html and ctrl. So basically my App is very small consistent of the Login and everything else is getting pulled from the Server depending on the Userrights.
What I use:
Cordova
AngularJs
Ionic Framework
Why I need it to be all dynamic:
1)The possiblity to have an App that contains just the login logic, so when fixing bugs or adding Modules I only have to add the files to the server give the User the right for it and it is there without needing to update the app.
2)The User only has the functionality he needs, he doesnt need to have everything when he only has the right for 1 module.
3)The App grows very big at the moment, meaning every Module has like 5-10 states, with their own html and Controllers. currently there are 50 different Modules planned so you can do the math.
I looked at this to get some inspiration:
AngularJS, ocLazyLoad & loading dynamic States
What I tried so far:
I created 1 Html file which contains the whole module so I only have 1 http request:
Lets say this is my response from the server after the User logged in
HTML Part:
var rights= [A,B,C,D]
angular.forEach(rights, function (value, key) {
$http.get('http://myServer.com/templates/' + value + '.html').then(function (response) {
//HTML file for the whole module
splits = response.data.split('#');
//Array off HTMl strings
for (var l = 1; l <= splits.length; l++) {
//Putting all Html strings into templateCache
$templateCache.put('templates/' + value +'.html', splits[l - 1]);
}
}
});
Controller Part:
var rights= [A,B,C,D]
angular.forEach(rights, function (value, key) {
$http.get('http://myServer.com/controller/' + value + '.js').then(function (response) {
// 1 file for the whole module with all controllers
splits = response.data.split('#');
//Array off controller strings
for (var l = 1; l <= splits.length; l++) {
//Putting all Controller strings into templateCache
$templateCache.put('controllers/' + value +'.js', splits[l - 1]);
}
}
});
After loading the Controllers I try to register them:
$controllerProvider.register('SomeName', $templateCache.get('controllers/someController));
Which is not working since this is only a string...
Defining the Providers:
.config(function ($stateProvider, $urlRouterProvider, $ionicConfigProvider, $controllerProvider) {
// turns of the page transition globally
$ionicConfigProvider.views.transition('none');
$stateProviderRef = $stateProvider;
$urlRouterProviderRef = $urlRouterProvider;
$controllerProviderRef = $controllerProvider;
$stateProvider
//the login state is static for every user
.state('login', {
url: "/login",
templateUrl: "templates/login.html",
controller: "LoginCtrl"
});
//all the other states are missing and should be created depending on rights
$urlRouterProvider.otherwise('/login');
});
Ui-Router Part:
//Lets assume here the Rights Array contains more information like name, url...
angular.forEach(rights, function (value, key) {
//Checks if the state was already added
var getExistingState = $state.get(value.name)
if (getExistingState !== null) {
return;
}
var state = {
'lang': value.lang,
'params': value.param,
'url': value.url,
'templateProvider': function ($timeout, $templateCache, Ls) {
return $timeout(function () {
return $templateCache.get("templates" + value.url + ".html")
}, 100);
},
'ControllerProvider': function ($timeout, $templateCache, Ls) {
return $timeout(function () {
return $templateCache.get("controllers" + value.url + ".js")
}, 100);
}
$stateProviderRef.state(value.name, state);
});
$urlRouter.sync();
$urlRouter.listen();
Situation so far:
I have managed to load the html files and store them in the templateCache, even load them but only if the states were predefined.What I noticed here was that sometimes lets say when I remove an item from a List and come back to the View the item was there again maybe this has something to do with cache I am not really sure...
I have managed to load the controller files and save the controllers in the templateCache but I dont really know how to use the $ControllerPrioviderRef.register with my stored strings...
Creating the states did work but the Controller didnt fit so i could not open any views...
PS: I also looked at require.js and OCLazyLoad as well as this example dynamic controller example
Update:
Okay so I managed to load the Html , create the State with the Controller everything seems to work fine, except that the Controller does not seem to work at all, there are no errors, but it seems nothing of the Controller logic is executed. Currently the only solution to register the controller from the previous downloaded file was to use eval(), which is more a hack then a proper solution.
Here the code:
.factory('ModularService', ['$http', ....., function ( $http, ...., ) {
return {
LoadModularContent: function () {
//var $state = $rootScope.$state;
var json = [
{
module: 'Calc',
name: 'ca10',
lang: [],
params: 9,
url: '/ca10',
templateUrl: "templates/ca/ca10.html",
controller: ["Ca10"]
},
{
module: 'SL',
name: 'sl10',
lang: [],
params: 9,
url: '/sl10',
templateUrl: "templates/sl/sl10.html",
controller: ['Sl10', 'Sl20', 'Sl25', 'Sl30', 'Sl40', 'Sl50', 'Sl60', 'Sl70']
}
];
//Load the html
angular.forEach(json, function (value, key) {
$http.get('http://myserver.com/' + value.module + '.html')
.then(function (response) {
var splits = response.data.split('#');
for (var l = 1; l <= value.controller.length; l++) {
$templateCache.put('templates/' + value.controller[l - 1] + '.html', splits[l - 1]);
if (l == value.controller.length) {
$http.get('http://myserver.com//'+value.module+'.js')
.then(function (response2) {
var ctrls = response2.data.split('##');
var fullctrl;
for (var m = 1; m <= value.controller.length; m++){
var ctrlName = value.controller[m - 1] + 'Ctrl';
$controllerProviderRef
.register(ctrlName, ['$scope',...., function ($scope, ...,) {
eval(ctrls[m - 1]);
}])
if (m == value.controller.length) {
for (var o = 1; o <= value.controller.length; o++) {
var html = $templateCache
.get("templates/" + value.controller[o - 1] + ".html");
var getExistingState = $state.get(value.controller[o - 1].toLowerCase());
if (getExistingState !== null) {
return;
}
var state = {
'lang': value.lang,
'params': value.param,
'url': '/' + value.controller[o - 1].toLowerCase(),
'template': html,
'controller': value.controller[o - 1] + 'Ctrl'
};
$stateProviderRef.state(value.controller[o - 1].toLowerCase(), state);
}
}
}
});
}
}
});
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
$urlRouter.listen();
}
}
}])
Any help appreciated
Ok, so let's start from the beginning.
All the application logic should be contained on the server and served via API-calls through REST, SOAP or similar. By doing so, you reduce the amount of logic built into the UI, which reduces the stress on the client. This basically makes your client app a rendering agent, containing only models and views for the data and logic served by the backend API.
As foreyez stated in his/her comment, this isn't an issue for any modern (or half-modern) device.
If you insist on not loading all of the layouts at once, you could of course separate them into partials, which you load after the login based on the user privileges. By doing so, you reduce the amount of in-memory data, even though the improvement would be doubtable, at best.
Can I suggest you to do some changes to the way you load the states?
Write a script that give you back a json with the states the user can access.
Ex.
resources/routing-config.yourLangage?user=user-id-12345
this will return a json file that depends on the user logged in. The structure can be something like this:
[
{
"name": "home",
"url": "/home",
"templateUrl": "views/home.html",
"controller": "HomeController",
"dependencies": ["scripts/home/controllers.js", "scripts/home/services.js", "scripts/home/directives.js"]
},
{
"name": "user",
"url": "/user",
"templateUrl": "views/user.html",
"controller": "UserController",
"dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
}
]
Then let's write a service that will read the states the user is allowed to access:
app.factory('routingConfig', ['$resource',
function ($resource) {
return $resource('resources/routing-config.yourLangage', {}, {
query: {method: 'GET',
params: {},
isArray: true,
transformResponse: function (data) {
// before that we give the states data to the app, let's load all the dependencies
var states = [];
angular.forEach(angular.fromJson(data), function(value, key) {
value.resolve = {
deps: ['$q', '$rootScope', function($q, $rootScope){
// this will be resolved only when the user will go to the relative state defined in the var value
var deferred = $q.defer();
/*
now we need to load the dependencies. I use the script.js javascript loader to load the dependencies for each page.
It is very small and easy to be used
http://www.dustindiaz.com/scriptjs
*/
$script(value.dependencies, function(){ //here we will load what is defined in the dependencies field. ex: "dependencies": ["scripts/user/controllers.js", "scripts/user/services.js", "scripts/home/directives.js"]
// all dependencies have now been loaded by so resolve the promise
$rootScope.$apply(function(){
deferred.resolve();
});
});
return deferred.promise;
}]
};
states.push(value);
});
return states;
}
}
});
}]);
Then let's configure the app:
app.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$filterProvider', '$provide', '$compileProvider',
function ($stateProvider, $urlRouterProvider, $locationProvider, $filterProvider, $provide, $compileProvider) {
// this will be the default state where to go as far as the states aren't loaded
var loading = {
name: 'loading',
url: '/loading',
templateUrl: '/views/loading.html',
controller: 'LoadingController'
};
// if the user ask for a page that he cannot access
var _404 = {
name: '_404',
url: '/404',
templateUrl: 'views/404.html',
controller: '404Controller'
};
$stateProvider
.state(loading)
.state(_404);
// save a reference to all of the providers to register everything lazily
$stateProviderRef = $stateProvider;
$urlRouterProviderRef = $urlRouterProvider;
$controllerProviderRef = $controllerProvider;
$filterProviderRef = $filterProvider;
$provideRef = $provide;
$compileProviderRef = $compileProvider;
//redirect the not found urls
$urlRouterProvider.otherwise('/404');
}]);
Now let's use this service in the app.run:
app.run(function ($location, $rootScope, $state, $q, routingConfig) {
// We need to attach a promise to the rootScope. This will tell us when all of the states are loaded.
var myDeferredObj = $q.defer();
$rootScope.promiseRoutingConfigEnd = myDeferredObj.promise;
// Query the config file
var remoteStates = routingConfig.query(function() {
angular.forEach(remoteStates, function(value, key) {
// the state becomes the value
$stateProviderRef.state(value);
});
// resolve the promise.
myDeferredObj.resolve();
});
//redirect to the loading page until all of the states are completely loaded and store the original path requested
$rootScope.myPath = $location.path();
$location.path('/loading'); //and then (in the loading controller) we will redirect to the right state
//check for routing errors
$rootScope.$on('$stateChangeError',
function(event, toState, toParams, fromState, fromParams, error){
console.log.bind(console);
});
$rootScope.$on('$stateNotFound',
function(event, unfoundState, fromState, fromParams){
console.error(unfoundState.to); // "lazy.state"
console.error(unfoundState.toParams); // {a:1, b:2}
console.error(unfoundState.options); // {inherit:false} + default options
});
});
Eventually, the LoadingController:
app.controller('LoadingController', ['$scope', '$location', '$rootScope',
function($scope, $location, $rootScope) {
//when all of the states are loaded, redirect to the requested state
$rootScope.promiseRoutingConfigEnd.then(function(){
//if the user requested the page /loading then redirect him to the home page
if($rootScope.myPath === '/loading'){
$rootScope.myPath = '/home';
}
$location.path($rootScope.myPath);
});
}]);
In this way everything is super flexible and lazy loaded.
I wrote 3 different user portals already and I can easily scale to all of the user portal I want.
I have developed an application with keeping those things in mind. Here is my architecture.
Folder Structure:
WebApp
|---CommonModule
|---common-module.js //Angular Module Defination
|---Controllers //Generally Nothing, but if you have a plan to
//extend from one CommonController logic to several
//module then it is usefull
|---Services //Common Service Call Like BaseService for all $http
//call, So no Module Specific Service will not use
//$http directly. Then you can do several common
//things in this BaseService.
//Like Error Handling,
//CSRF token Implementation,
//Encryption/Decryption of AJAX req/res etc.
|---Directives //Common Directives which you are going to use
//in different Modules
|---Filters //Common Filters
|---Templates //Templates for those common directives
|---index.jsp //Nothing, Basically Redirect to
//Login or Default Module
|---scripts.jsp //JQuery, AngularJS and Other Framworks scripts tag.
//Along with those, common controlers, services,
//directives and filtes.
|---templates.jsp //Include all common templates.
|---ng-include.jsp //will be used in templates.jsp to create angular
//template script tag.
|---ModuleA
|---moduleA-module.js //Angular Module Definition,
//Use Common Module as Sub Module
|---Controllers
|---Services
|---Directives
|---Filters
|---Templates
|---index.jsp
|---scripts.jsp
|---templates.jsp
|---ModuleB
|--- Same as above ...
Note: Capital Case denotes folder. Beside ModuleA there will a LoginModule for your case I think or You could Use CommonModule for it.
Mehu will be as follows.
Module A <!--Note: index.jsp are indexed file
//for a directive -->
Module B
Each of those JSP page are actually a independent angular application. Using those following code.
ModuleA/index.jsp
<!-- Check User Permission Here also for Security
If permission does not have show Module Unavailable Kind of JSP.
Also do not send any JS files for this module.
If permission is there then use this following JSP
-->
<!DOCTYPE HTML>
<html lang="en" data-ng-app="ModuleA">
<head>
<title>Dynamic Rule Engine</title>
<%# include file="scripts.jsp" %>
<%# include file="templates.jsp" %> <!-- Can be cached it in
different way -->
</head>
<body>
<%# include file="../common.jsp" %>
<div id="ngView" data-ng-view></div>
<%# include file="../footer.jsp" %>
</body>
</html>
ModuleA/scripts.jsp
<%# include file="../CommonModule/scripts.jsp" %> <!-- Include Common Things
Like Jquery Angular etc -->
<scripts src="Controlers/ModlueAController1.js"></script>
.....
ModuleA/templates.jsp
<%# include file="../CommonModule/templates.jsp" %>
<!-- Include Common Templates for common directives -->
<jsp:include page="../CommonModule/ng-include.jsp"><jsp:param name="src" value="ModuleA/Templates/template1.jsp" /></jsp:include>
.....
CommonModule/ng-include.jsp
<script type="text/ng-template" id="${param.src}">
<jsp:include page="${param.src}" />
</script>
But main problem of this approach is When user will change Module, Page will get refreshed.
EDIT:
There is a ModuleA.module.js file which actually contain module deceleration as follows.
angular.module('ModuleA.controllers', []);
angular.module('ModuleA.services', []);
angular.module('ModuleA.directives', []);
angular.module('ModuleA.filters', []);
angular.module('ModuleA',
['Common',
'ModuleA.controllers' ,
'ModuleA.services' ,
'ModuleA.directives' ,
'ModuleA.filters'])
.config(['$routeProvider', function($routeProvider) {
//$routeProvider state setup
}])
.run (function () {
});
I think I'm doing what you're asking. I achieve this by using UI-Router, ocLazyLoad and ui-routers future states. Essentially our setup allows us to have 50+ modules, all in the same code base, but when a user opens the app. its starts by only loading the base files required by the app. Then, as the user moves around between states, the application will load up the files required for that part, as their needed. (apologies for the fragmented code, I've had to rip it out of the code base, but tried to only provide the stuff thats actually relevant to the solution).
Firstly, the folder structure
Core App
config.js
Module 1 (/module1)
module.js
controllers.js
Module 2 (/module2)
module.js
controllers.js
etc
Config.js:
The first thing we do is create the base state, this is an abstract state, so the user can never actually just hit it.
$stateProvider.state('index', {
abstract: true,
url: "/index",
views: {
'': {
templateUrl: "views/content.html" // a base template to have sections replaced via ui-view elements
}
},
...
});
Then we configure the modules in ocLazyLoad. This allows us to just tell ocLazyLoad to load the module, and it loads all the required files (although in this instance, its only a single file, but it allows each module to have varying paths).
$ocLazyLoadProvider.config({
loadedModules: ['futureStates'],
modules: [
{
name: 'module1',
files: ['module1/module.js']
},
{
name: 'module2',
files: ['module2/module.js']
}
]
});
Next we create a function to allow ui-router to load the modules when requested (through future states).
function ocLazyLoadStateFactory($q, $ocLazyLoad, futureState) {
var deferred = $q.defer();
// this loads the module set in the future state
$ocLazyLoad.load(futureState.module).then(function () {
deferred.resolve();
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
}
$futureStateProvider.stateFactory('ocLazyLoad', ['$q', '$ocLazyLoad', 'futureState', ocLazyLoadStateFactory]);
Then we configure the actual future states. These are states that may be loaded in the future, but we don't want to configure them right now.
$futureStateProvider.futureState({
'stateName': 'index.module1', // the state name
'urlPrefix': '/index/module1', // the url to the state
'module': 'module1', // the name of the module, configured in ocLazyLoad above
'type': 'ocLazyLoad' // the future state factory to use.
});
$futureStateProvider.futureState({
'stateName': 'index.module2',
'urlPrefix': '/index/module2',
'module': 'module2',
'type': 'ocLazyLoad'
});
If you want the list of future states to be provided asynchronously:
$futureStateProvider.addResolve(['$http', function ($http) {
return $http({method: 'GET', url: '/url'}).then(function (states) {
$futureStateProvider.futureState({
'stateName': 'index.module2',
'urlPrefix': '/index/module2',
'module': 'module2',
'type': 'ocLazyLoad'
});
});
}]);
Then we configure the modules as follows:
module1/module.js
$stateProvider.state('index.module1', {
url: "/module1",
abstract: true,
resolve: {
loadFiles: ['$ocLazyLoad', function($ocLazyLoad){
return return $ocLazyLoad.load(['list of all your required files']);
}]
}
})
$stateProvider.state('index.module1.sub1', {
url: "/sub1",
views: {
// override your ui-views in here. this one overrides the view named 'main-content' from the 'index' state
'main-content#index': {
templateUrl: "module1/views/sub1.html"
}
}
})
I am trying to test an Angular directive that has an external template, but cannot get this to work. This is my first attempt at using Karma. I have Googled for a solution, and tried various changes to the karma.conf.js file, but still keep getting this:
Error: [$injector:modulerr] Failed to instantiate module app/inventory/itemDetails.html due to:
Error: [$injector:nomod] Module 'app/inventory/itemDetails.html' is not available! You either misspelled the module name or forgot to load it.
Folder structure:
app
inventory
itemDetails.html
itemDetailsDirective.js (templateUrl: "app/inventory/itemDetails.html")
UnitTest
karma.conf.js
specs
inventorySpec.js
karma.conf.js
// list of files / patterns to load in the browser
files: [
'../scripts/jquery.min.js',
'scripts/angular.js',
'scripts/angular-mocks.js',
'../app/*.js',
'../app/**/*.js',
'../app/inventory/itemDetails.html',
'specs/*.js'
],
preprocessors: {
'../app/inventory/itemDetails.html':['ng-html2js'] // Is this supposed to be the path relative to the karma.conf.js file?
},
ngHtml2JsPreprocessor: {
stripPrefix: '../',
},
itemDetailsDirective.js
templateUrl: "app/inventory/itemDetails.html",
inventorySpec.js (most stuff commented out for debug purposes)
describe("itemDetailsDirective", function () {
var element, $scope;
beforeEach(module("app/inventory/itemDetails.html"));
beforeEach(inject(function ($compile, $rootScope) {
console.log("itemDetailsDirective");
//element = angular.element('<item-details></item-details>');
//$scope = $rootScope.$new();
//$compile(element)($scope);
//$scope.$digest();
}));
it('should display', function () {
// var isolatedScope = element.isolateScope();
// //expect(isolatedScope.condition).toEqual("Fair");
});
});
So I have a UnitTest folder (VS 2013 project) parallel to the app folder. The paths under "files" in karma.conf.js are correct - all "non-templateUrl" tests work fine.
Help would be great, while I still have some hair left!
I got this working, thanks to this article that I just came across: http://willemmulder.net/post/63827986070/unit-testing-angular-modules-and-directives-with
The key is that the paths are relative to the DISK root!
To illustrate, I changed the karma.conf.js file to use cahceIdFromPath:
ngHtml2JsPreprocessor: {
cacheIdFromPath : function(filepath) {
console.log("karma, cacheIdFromPath " + filepath);
var templateFile = filepath.substr(filepath.indexOf("/app/") + 1 );
console.log("karma, templateFile: " + templateFile);
return templateFile;
},
},
Output:
karma, cacheIdFromPath C:/Dev/BcCom/app/inventory/itemDetails.html
karma, templateFile: app/inventory/itemDetails.html
With the above, it now works as it should!
I am not sure what do you want to test but you are trying to use an html as module which I think is not correct and probably you have to use $httpbackendto mock the GET request. I made a dummy example:
'use strict';
describe('TestMyDirective', function() {
var $compile, $http, $httpBackend, $rootScope, $scope, template;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
// Inject $httpBackend service
$httpBackend = $injector.get('$httpBackend');
// Mock request to your html
$httpBackend.whenGET('my.html').respond("GotIT");
$scope = $rootScope.$new();
template = $compile("<my-directive> </my-directive>")($scope);
$scope.$digest();
}));
/*
*
* Tests
*
*/
describe('Test something', function() {
it('should test something', function() {
// You can test that your directive add something on the $scope
});
});
});
I am trying to add custom page CSS/JS to my partials via controllers, i have created a custom directive for it but when i am loading the CSS using it it gets unnecessary parameters which i don't want, and when i load JS using it my JS does not load the the right time(meaning the init functions written in the page gets called before, leaving the error)
DIRECTIVE
app.directive('head', ['$rootScope','$compile',
function($rootScope, $compile){
return {
restrict: 'E',
link: function(scope, elem){
var html = '<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="{{cssUrl}}" />';
elem.append($compile(html)(scope));
scope.routeStyles = {};
$rootScope.$on('$routeChangeStart', function (e, next, current) {
if(current && current.$$route && current.$$route.css){
if(!Array.isArray(current.$$route.css)){
current.$$route.css = [current.$$route.css];
}
angular.forEach(current.$$route.css, function(sheet){
delete scope.routeStyles[sheet];
});
}
if(next && next.$$route && next.$$route.css){
if(!Array.isArray(next.$$route.css)){
next.$$route.css = [next.$$route.css];
}
angular.forEach(next.$$route.css, function(sheet){
scope.routeStyles[sheet] = sheet;
});
}
});
}
};
}
]);
APP CONTROLLER
app.config(['$routeProvider', function($routeProvider){
$routeProvider
.when('/some/route/1', {
templateUrl: 'partials/partial1.html',
controller: 'Partial1Ctrl',
css: 'css/partial1.css'
})
.when('/some/route/2', {
templateUrl: 'partials/partial2.html',
controller: 'Partial2Ctrl',
css: ['css/partial2_1.css','css/partial2_2.css']
})
}]);
Output I am getting
CSS
<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="css/createtroll.css" class="ng-scope" href="css/partial2.css">
HTML
<script ng-repeat="(routeCtrl, scriptUrl) in routeScript" ng-src="js/page.css" class="ng-scope" src="css/scroll.js"></script>
I want know how to remove the ng-directives form the output and how to load js before document.ready so that I don't get the error. Any help is much appreciated.
it seems that you are looking for a way to lazy-load some aspect of our AngularJS application by loading JS and CSS files asynchronously.
You cannot use controllers/directives for loading your files because your app has already been bootstrapped.
You'll need some kind of file/module loader, for example you can use RequireJS - a JavaScript file and module loader.
there are already a lot of examples and implementation of RequireJS and AngularJS.
Example:
require.config({
baseUrl: "js",
paths: {
'angular': '.../angular.min',
'angular-route': '.../angular-route.min',
'angularAMD': '.../angularAMD.min'
},
shim: { 'angularAMD': ['angular'], 'angular-route': ['angular'] },
deps: ['app']
});
Reference:
http://marcoslin.github.io/angularAMD/#/home
http://www.startersquad.com/blog/angularjs-requirejs/
http://www.bennadel.com/blog/2554-Loading-AngularJS-Components-With-RequireJS-After-Application-Bootstrap.htm
http://requirejs.org/