How to synchronously bootstrap an angularjs app
I define a couple constant values on my app object. Some of these values need to be set via a call to a service and these calls need to complete before any of my controllers are instantiated.
In the example below, I define an array of values on an object named config. I need to set the value named PostIndexMenuID prior to any of my controllers being instantiated. How do I do that?
I have tried manually bootstrapping (removing ng-app from the html). I am not using routing.
Ideally I will not have to learn, download, install, configure, test, and maintain another framework to accomplish this.
(function()
{
angular.module('app', []);
var app = angular.module('app');
app.controller('controller', ['$scope', 'config', '$http', controller]);
app.service('menusService', ['$http', 'config', menusService]);
// Create config object. Some values are set in app.run
app.value('config', {
siteID: 100,
webRoot: '',
apiRoot: '/api',
imageRoot: '/Content/images',
PostIndexMenuID: 0
});
app.run(['$http', 'config','menusService', function ($http, config, menusService) {
menusService.GetMenuIDByName("PostIndex", function (data) {
config.PostIndexMenuID = data; // Need to complete this BEFORE GetPosts on the controller is called
});
}]);
function controller($scope, config, $http) {
var vm = this;
vm.Posts = 0;
function GetPosts() {
// In prod we call a service here get posts based on config.PostIndexMenuID
// for this example just return PostIndexMenuID.
vm.Posts = config.PostIndexMenuID;
};
GetPosts(); // need to delay calling this until AFTER PostIndexMenuID is set
};
function menusService($http, config) {
this.GetMenuIDByName = function (menuName, callBack) {
var uri = config.apiRoot + '/menu/GetMenuByName?menuName=' + menuName + '&siteID=' + config.siteID;
// use a timeout to simulate a slow service for this example and return a dummy value
var menuID = 99;
setTimeout(function () {
callBack(menuID);
}, 2000);
};
};
})()
// html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="app" >
<head>
<title></title>
</head>
<body >
<div ng-controller = "controller as vm">
<p>PostIndexMenuId is {{ vm.Posts }}</p>
</div>
<script src="Scripts/jquery-1.8.2.js"></script>
<script src="Scripts/angular.js"></script>
<script src="Scripts/angular-route.js"></script>
<script src="app/app.js"></script>
</body>
</html>
There is quite a nifty trick in Angular.js whereby you can defer the changing of a route until all promises have been resolved. You may have to restructure your application a little bit to cater for this approach, but I use it myself and it works like a treat!
$rootScope.$on('$routeChangeStart', function $routeChangeStart(event, next) {
// Extend the resolution of the route promise to wait for the user authentication
// process to determine if there's a valid session available.
next.resolve = angular.extend( next.resolve || {}, {
authenticating: api.isSession
});
});
By using the next.resolve you're extending Angular's sequence of promises that are resolved before a route change is considered a success. In the above case, as long as api.isSession returns a promise then everything will work wonderfully.
As soon as the api.isSession promise has been resolved, then the route change will succeed, and in your case, your controller will be loaded.
Related
Explaining the problem:
So in the current app we have a couple of constant configuration declarations that connects the app to either the production or development environment, and we comment one out whenever we want to switch which doesn't seem like the ideal scenario to me. So what I was after is having a configuration external json file that contains the values and have that file separately from the changing code and get values from there into my constant.
The actual question:
In this piece of code:
application.constant('servicesConfig', (function() {
var con = 'appdev';
//var con = 'appprod';
return {
host: con+'.appdomain.com'
}
}
As you can see I have to modify the 'con' variable manually in order to switch between the dev and prod environments, instead, I want to do the following:
application.constant('servicesConfig', (function() {
var deferred = $q.defer();
var configLocation = 'config/server.json';
var configurations = $http.get(configLocation)
return {
host: configurations.con+'.appdomain.com'
}
}
My question is how can I get the $http or other angular services injected?
You can manually bootstrap angular after receive data from server.
Example on jsfiddle:
var app = angular.module("myApp", []);
app.controller("myController", function($scope, servicesConfig) {
console.log(servicesConfig);
$scope.model = servicesConfig.host;
$scope.reset = function() {
$scope.model = '';
};
});
angular.element(document).ready(function() {
//Simulate AJAX call to server
setTimeout(function() {
app.constant('servicesConfig', {
host: "appdev"
});
angular.bootstrap(document, ['myApp']);
}, 2000);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-cloak>
<div ng-controller="myController">
<input type="text" ng-model="model" /> {{model}}
<button ng-click="reset()">Reset</button>
</div>
</div>
I'm using angularjs with asp.net mvc. On my page, I have 5 dropdown lists, all populated with values from different database tables. In order to retrieve the data for all dropdowns, I'm making 5 different $http.get requests, which makes my page load time slow to a crawl. I know this is setup wrong, but not sure how to do it properly. Here's my angular code, which makes a call to an mvc action, returns the values for the dropdown & passes the results into the $scope, for the dropdown to display:
var CustomSearchController = function ($scope, $http) {
$http.get('/Account/GetLists')
.success(function (result) {
$scope.listDetails = result;
})
};
$http.get('/Account/GetGenders')
.success(function (result) {
$scope.genderDetails = result;
})
};
$http.get('/Account/GetEthnicities')
.success(function (result) {
$scope.ethnicityDetails = result;
})
};
$http.get('/Account/GetRegions')
.success(function (result) {
$scope.regionDetails = result;
})
};
$http.get('/Account/GetAcademics')
.success(function (result) {
$scope.academicDetails = result;
})
};
What's the correct way to go about this?
You should use $httpProvider.useApplyAsync and pass in true.
From the documentation:
Configure $http service to combine processing of multiple http responses received at around the same time via $rootScope.$applyAsync. This can result in significant performance improvement for bigger applications that make many HTTP requests concurrently (common during application bootstrap).
This means that if true, when requests are loaded, they will schedule a deferred "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window to load and share the same digest cycle.
In short, you can avoid unnecessary digest cycles which can make a hell of a difference.
Simply pass $httpProvider into your run function and modify the provider like so:
angular.module('MyApp', [])
.run(function($httpProvider){
$httpProvider.useApplyAsync(true);
});
You can use $q service for such a scenario like this:
var CustomSearchController = function ($scope, $q, $log, $http) {
var listPromise = $http.get('/Account/GetLists');
var genderPromise = $http.get('/Account/GetGenders');
var ethnicCitiesPromise = $http.get('/Account/GetEthnicities')
var regionsPromise = $http.get('/Account/GetRegions')
var academicPromise = $http.get('/Account/GetAcademics');
$q.all([genderPromise, ethnicCitiesPromise, regionsPromise, academicPromise])
.then(function (responses) {
// Here you have all the resolved datas
// responses[0]
// responses[1]
// responses[2]
// responses[3]
}).catch(function (error) {
$log.error(error);
throw error;
})
}
However I prefer to call a function of ng-change of each dropdown to retrieve data in a cascade style.
Try like this:
your Controller (main view):
ViewBag.regionDetailsJson = new JavaScriptSerializer().Serialize(regionDetails );
In your view:
var regionDetailsJson = $.parseJSON('#Html.JsonRaw((string)ViewBag.regionDetailsJson )');
And than in your angular controller:
$scope.regionDetailsJson = regionDetailsJson;
Using promises as suggested by HFA is a more Angular approach, but it will still result in 5 calls to the server; so will not really help improve your page load performance - i.e you will still be waiting for the 5 promises to resolve before you get your data.
One thing you could try is to formulate this lists when the page served by your MVC Account action is loaded and save these to the page model. You can then access this client side, in a script block, assigning them to a (global)variable, which you can then access in Angular.
I've not tested this but have you this approach in the past. As a guide the steps you would need to follow are:
In your AccountController
public ActionResulst Index() {
var model = new AccountModel {
Lists = GetLists(),
Ethnicities = GetLists()
}
return View(model);
}
Then on your Razor Page
#model AccountModel
<script>
window.lists= "#(Model.Lists)";
window.ethnicities = "#(Model.Ethnicities )";
</script>
Then in your Angular
var CustomSearchController = function ($window) {
var lists = $window.lists;
var ethnicities = $window.ethnicities;
}
You may need to convert the results to JSON using JSON.Parse
There is nothing wrong with your approach. You are following the separation of concern, and single responsibility.
If you get the date from Model or ViewModel from ASP.Net MVC View, you cannot Unit Test the Angular Services easily.
Another approach is to use resolve. It basically load the data first before a route executes. FYI: It will not improve the overall loading time, although you can display page loading icon.
Demo for Angular 1.5 Route Resolve at Plunker
(function () {
angular
.module("app", ['ngRoute'])
.config(function($routeProvider){
$routeProvider
.when("/home", {
template: `<main
promise-followers="$resolve.promiseFollowers"></main>`,
resolve: {promiseFollowers: function ($http) {
return $http.get("https://api.github.com/users/octocat/followers")
.then(function(result) {
return result.data;
}, function(result) {
console.log(result);
});
}
},
})
.otherwise({ redirectTo: "/home" } );
})
.component("main", {
template: `<h3>Demo Angular 1.5 Resolver</h3>
<p>Promise Data from Resolve :
<select ng-model="$ctrl.selected"
ng-options="option.id as option.login for option in $ctrl.promiseFollowers"></select>
</p>
<h4>Regular Selector Selected: {{$ctrl.selected}}</h4>`,
bindings: {
promiseFollowers: '='
},
controller: function(){
}
});
})();
<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<link data-require="bootstrap-css#3.3.6" data-semver="3.3.6" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<script>document.write('<base href="' + document.location + '" />');</script>
<script src="https://code.angularjs.org/1.5.7/angular.js"></script>
<script src="https://code.angularjs.org/1.5.7/angular-route.js"></script>
<script src="app.js"></script>
</head>
<body>
<ng-view></ng-view>
</body>
</html>
I have initialized my app following these two guides:
AngularJS + Cloud Endpoints -- A Recipe for Building Modern Web Applications
Angular Js and google api client.js (gapi)
My setup looks something like this:
app.js
function init() {
console.log('running global init()');
window.initgapi();
}
var app = angular.module('app', []);
// Main Controller
app.controller('MainCtrl', ['$scope', '$window', 'cloudendpoints', function($scope, $window, cloudendpoints) {
// this is called once eventapi is loaded
var postInit = function() {
$scope.backend_ready = true;
$scope.fetchContent();
};
$window.initgapi = function() {
cloudendpoints.init(postInit);
};
$scope.fetchContent = function() {
gapi.client.cloudendpoints
.getContent()
.execute(function(resp) {
// do something with response
});
};
}]);
The cloudendpoints service is in its own file, cloudendpoints.js:
// for loading endpoints service
app.factory('cloudendpoints', [function cloudendpoints() {
var init = function(postInit) {
var restUrl = '//' + window.location.host + '/_ah/api';
gapi.client.load('cloudendpoints', 'v1', postInit, restUrl);
};
return { init: init };
}]);
Lastly, our scripts are loaded in this order:
<script src="angular.min.js"></script>
<script src="app.js"></script>
<script src="controllers/mainCtrl.js"></script>
<script src="services/cloudendpoints.js"></script>
<script src="https://apis.google.com/js/client.js?onload=init"></script>
The Challenge
This works well so far, because we are only using a single controller (MainCtrl). This is what happens in our code:
The gapi client loads, then calls init(), which calls window.loadCloudEndpoints()
cloudendpoints.init(postInit) loads the cloudendpoints endpoint service, which then calls the postInit() callback. We can then make calls to the endpoints API from within the postInit().
The challenge arises when we want to create another controller to handle another view of our app. Let's say we create a ContentPageCtrlcontroller — do we then need to once again init our endpoint service? How can we make the endpoint service available to all controllers without having to repeat ourselves?
My Hacky Solution
In order to get around this, I $watch the the backend_ready so that I may only start making gapi calls after the endpoints api has loaded:
app.controller('ContentPageCtrl', ['$scope', function($scope) {
/**
* Make sure that the backend is ready before
* running any gapi.client.cloudendpoints calls
**/
$scope.$watch('backend_ready', function() {
if ($scope.backend_ready === true) {
gapi.client.cloudendpoints
.loadContent()
.execute(function(resp) {
// put content in DOM
});
}
});
}]);
This means I would need to $watch the backend_ready variable in every controller in which I need to make endpoint calls. My approach feels pretty dirty, and has problems scaling.
What is the better approach to this?
A better approach to this would be to leverage the power of Promises. you can then have your Service init once (inside the service function) and on every method you simply call promise.then(...) and keep the logic specific to this method. take this as an example:
app.factory('cloudendpoints', ['$q','$timeout','$window',function cloudendpoints($q,$timeout,$window) {
var backend_ready = $q.defer();
checkLoaded();
function checkLoaded(){
if($window.gapi)
backend_ready.resolve();
else
$timeout(checkLoaded,100); //check again in 100ms
}
var init = function(postInit) {
var restUrl = '//' + window.location.host + '/_ah/api';
return backend_ready.promise.then(function(resp){
gapi.client.load('cloudendpoints', 'v1', postInit, restUrl);
}); //we are returning a promise so we can have more
//flexability inside the controllers (do stuff after the loaded api);
};
return {
init: init
};
}]);
//Somewhere inside a controller...
app.controller('someCtrl', ['cloudendpoints', function(cloudendpoints){
function postInit(){ ... }
cloudendpoints.init(postInit); //waits for gapi to load, then executes init
});
I am relatively new to JavaScript & Angular.
So this may be a dumb question, but here goes ...
I need to execute a function that will perform data transformation on incoming data and create arrays & objects that the page will consume. I can only process my page after this function is executed.
Please note that this function will not be used directly by any angular artifact, but its output will.
Please advise. Thanks
$scope.prepped_data = function (data) {
// code to generate new data structures //
$scope.data1 = {};
$scope.data2 = {};
};
Just create a factory like this:
angular.module('yourAppModule').factory('yourFactory', function ($http) {
return {
getData: function() {
var url = 'http://get-your-data.com/data';
return $http({
method: 'GET',
url: url
});
},
}
And afterwards, in the controller:
angular.module('yourAppModule').controller('yourCtrl', function ($scope, yourFactory) {
yourFactory.getData().success(function (data) {
// process your data here
$scope.data = dataProcessed;
}
});
You can then use "data" in your view to display it as you wish, there are also filtering capabilities in Angular templates. Take a look.
If you need a custom function to be used in the view, you may pass it to the $scope in your controller, kinnda like you did.
$scope.myFunc = function(data){
//code here
}
And then in your view, you may use:
<!doctype html>
<html xmlns:ng="http://angularjs.org" ng-app="yourAppModule" id="ng-app">
<head>
<script src="bower_components/angular/angular.js"></script>
</head>
<body>
<div ng-controller="yourCtrl">
{{ myFunc() }}
</div>
</body>
</html>
Hope it helps,
Best.
In the service
public method1(){
var defer = this.$q.defer();
//Show some loader
this.$http({
method: 'GET',
url: "www.xyz.com"
}).success((result) => {
//Hide loader
defer.resolve(result);
}).error((result) => {
defer.reject(result);
});
In the controller
method1().then(function(data){//Do your processing})
I have a simpe google-chrome extension app. I'm using bookmarks and it present in manifest file. Firstly, i use chrome bookmarks api in controller, and it works well. But i decided use factory for clear code and best practices.
My index.html file
<!DOCTYPE html>
<html lang="en" ng-app="BookmarksSharer" ng-csp>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>bokmarks sharer</title>
<link rel="stylesheet" href="/css/main.css"/>
<script src="/javascript/jquery-2.1.1.min.js"></script>
<script src="/javascript/angular.min.js"></script>
<script src="/javascript/bookmark_sharer.js"></script>
<script src="/javascript/MainController.js"></script>
<script src="/javascript/BookmarkService.js"></script>
</head>
<body>
<div id="main_popup" ng-controller="MainController">
<p>Bookmarks</p>
<ul>
<li ng-repeat="bookmark_folder in bookmarks_folders" id="{{bookmark_folder.title}}">
{{bookmark_folder.title}}
<ul ng-repeat="bookmarks in bookmark_folder">
<li ng-repeat="bookmark in bookmarks | filter">{{bookmark.title}}</li>
</ul>
</li>
</ul>
</div>
</body>
</html>
bookmark_sharer.js is simple
var app = angular.module("BookmarksSharer", []);
MainController.js very simple too.
app.controller('MainController',['$scope', 'bookmarkFactory', function($scope, bookmarkFactory){
$scope.boormarks_folders = bookmarkFactory.folders;
}]);
And my factory
app.factory('bookmarkFactory', function () {
var bookmarks_folders;
function allBookmarksFolders(){
chrome.bookmarks.getTree(function(bookmarks_tree){
bookmarks_folders = bookmarks_tree[0].children;
});
return bookmarks_folders;
}
return {
folders: allBookmarksFolders()
};
});
Why $scope.bookmarks_folders is undefined?
ExpertSystem your code not working too. Simple solution is
app.controller('MainController',['$scope', function($scope){
chrome.bookmarks.getTree(function(nodes){
$scope.bookmarks_folders = nodes[0].children;
$scope.$apply();
})
}]);
But i want organize my code using factories or services.
chrome.bookmarks.getTree() is asynchronous, so by the time the getAllBookmarks() function returns, bookmarks_folders is undefined.
Your service exposes a property (folders) which is bound to the result of allBookmarksFolders(), alas undefined.
Since this operation is asynchronous, your service should return a promise instead, so the controller can use that promise and get the actual data when it is returnd:
// The service
app.factory('bookmarkFactory', function ($q) {
function retrieveAllBookmarksFolders() {
var deferred = $q.defer();
chrome.bookmarks.getTree(function (bookmarks_tree) {
deferred.resolve(bookmarks_tree[0].children);
});
return deferred.promise;
}
return {
foldersPromise: retrieveAllBookmarksFolders()
};
});
// The controller
app.controller('MainController', function($scope, bookmarkFactory){
bookmarkFactory.foldersPromise.then(function (bookmarks_folders) {
$scope.boormarks_folders = bookmarks_folders;
});
});
The main problem arises from the fact that (in your implementation) you return the value bound to bookmarks_folders at that point and later on re-assign bookmarks_folders to hold a reference to a different object (bookmarks_tree[0].children).
The flow of events inside your service is somewhat like this:
bookmarks_folders is declared (and initialized to undefined).
allBookmarksFolders() is executed and returns (the still undefined) bookmarks_folders.
folders is assigned a reference to the object currently referenced by bookmarks_folders (i.e. undefined).
The getTree() callback executes and assigns to bookmarks_folders a reference to a different object (bookmarks_tree[0].children). At that point folders knows nothing about it and continues to reference to the previous value (undefined).
An alternative approach (one that is used by the $resource btw), is to not assign a new reference to bookmarks_folders, but to modify the already referenced object.
// The service
app.factory('bookmarkFactory', function () {
var bookmarks_folders = []; // initialize to an empty array
function allBookmarksFolders(){
chrome.bookmarks.getTree(function(bookmarks_tree){
// Here we need to modify the object (array)
// already referenced by `bookmarks_folders`
// Let's empty it first (just in case)
bookmarks_folders.splice(0, bookmarks_folders.length);
// Let's push the new data
bookmarks_tree[0].children.forEach(function (child) {
bookmarks_folders.push(child);
});
});
return bookmarks_folders;
}
return {
folders: allBookmarksFolders()
};
});