I have a chart generator plugin built with AngularJS and d3js, I want to load some charts based on a priority ranking, for example:
There are 6 charts with 3 priority levels:
First priority: chart #1 & chart #5
Second priority: chart #3
Third priority: chart #2, chart #4 & chart #6
Note that you can't view a Second priority level chart before all of the first Priority charts have completely loaded.
How can I use lazy loading with my scope?
Here is my code:
Controller
ngCharts.controller('WgListMixed', function ($rootScope, $scope, $http, $sce, $timeout, $compile, SharedDateScopes) {
SharedDateScopes.getWidgetMixed(2).then(function(data_promised) {
$rootScope.wgMixLists = data_promised;
angular.forEach($rootScope.wgMixLists, function(widget) {
var scope_name = widget.wg_id;
var scope_name_v = widget.wg_id+"_v";
$rootScope[scope_name] = widget.data[0];
$rootScope[scope_name_v] = widget.data;
});
});
});
Serivce
ngCharts.service('SharedDateScopes', function($rootScope, $http, $q) {
return {
getWidgetMixed: function(dateRanges,stages) {
var dataRange = <%=$data_range%>;
if(typeof dateRanges !=="undefined") {
var dataRange = dateRanges;
}
var date_range_attr = "&date_range="+dataRange+"";
if (typeof codename == "undefined") {
var date_range_attr = "";
}
var defer = $q.defer();
$http.get('<%=[byt_control_root]%>/byt_reporting_widgets_manager?stage=mixed'+date_range_attr+'&widgets=<%=$bayt_mixed_widget%>').success(function (datas) {
defer.resolve(datas);
});
return defer.promise;
}
};
});
Well this isn't as much a lazy loading problem as it is a timing problem.
Imagine you maintain a list for each level.
var firstPriority = [ {ID : 1, loaded : false} , {ID : 5, loaded : false} ] ;
you start loading the data for 1 and 5 with promises of course. When each one finishes loading you change the flag in the array and then check the array to see if all elements in the array are now set to true. If they are then you start loading the next level, then the next.
This should do it and work for however many levels you have.
Related
I've got a sequence of json files (page1.json, page2.json, etc) that contain the layout details for our pages.
Up until now I've been loading the data for each page when it's needed and everything has been working well, but it's now clear that the page data for every page is needed when the angular app starts - and each page must be in a separate file (to please the designers).
Here's what I've been trying:
var app = angular.module("templatesApp", ['bulletComponent', 'popupComponent', 'imageComponent', 'paragraphComponent', 'ngRoute']);
var numberOfPages = 0;
var pageData = [];
var pageIndex = 0;
/*Factory that runs http.get on content.json, used by multiple controllers */
app.factory('content', ['$http', 'completions', 'pages', function($http, completions, pages) {
var url = 'json/content.json';
return $http.get(url)
.success(function(data) {
// loop through pages and add corresponding page objects to completions.js service
for (var i=0; i<data.length; i++) {
completions.addPage(data[i].title);
numberOfPages += 1;
}
loadPageData(pages, completions);
return data;
})
.error(function(data) {
return data;
});
}]);
var loadPageData = function(pages, completions) {
console.log("loadPageData called when pageIndex = "+pageIndex);
pages.success(function(loadedData) {
pageData.push(loadedData);
completions.addPageComponents(pageIndex + 1, pageData[pageData.length-1]);
pageIndex += 1;
if (pageIndex < numberOfPages) {
loadPageData(pages, completions);
}
});
}
app.factory('pages', ['$http', function($http) {
console.log("pages factory called with pageIndex = "+pageIndex);
var url = 'json/page'+ (pageIndex + 1) +'.json';
return $http.get(url)
.success(function(loadedData) {
return loadedData;
})
.error(function(loadedData) {
return loadedData;
});
}]);
/*Controller for menu, data fetched from factory*/
app.controller('menuCtrl', ['$scope', '$http', 'content', function($scope, $http, content) {
content.success(function(data) {
$scope.pages = data;
});
}]);
/*Controller for individual pages. Makes XHR to json page for pages data.
Has function for calculating column widths*/
app.controller('pageCtrl', ['$scope', '$routeParams', '$http', 'content', 'completions', function($scope, $routeParams, $http, content, completions) {
$scope.page = pageData[$routeParams.pageId-1];
$scope.getStyle = function(singleCase, device) {
if (singleCase == undefined)
return '';
return assignBootstrap(singleCase, device);
}
$scope.pageId = $routeParams.pageId;
}]);
So hopefully you can see, the 'content' factory gets called by 'menuCtrl'. It first loads the content.json file (a list of the page titles) then it calls 'loadPageData', a recursive function that should call the 'pages' factory on every required page, as it loads.
The problem is, those console.logs show that the 'pages' factory is getting called just once and BEFORE even the loadPageData function. Each html page is then being passed the same (first) set of data.
I presume I'm not using the 'pages' factory correctly, but I don't know what the correct options are.
If anyone has any pointers, I'd be keen to hear them.
EDIT
Thanks for the comments. I think I've realised what I need to know is: How do I set up a chain of promises ($http calls) dynamically? The number of pages will vary, so I can't hard-code them.
Here's what I ended up doing. Not sure if this is 100% best practise, but it seems to be working well:
app.factory('content', ['$http', 'completions', function($http, completions) {
var url = 'json/content.js';
return $http.get(url)
.success(function(data) {
// loop through pages and add corresponding page objects to completions.js service
for (var i=0; i<data.length; i++) {
completions.addPage(data[i].title);
numberOfPages += 1;
}
// load page .js files
var tempData = getData(0, completions);
for (var i = 1; i < numberOfPages; i++) {
(function (i) {
tempData = tempData.then(function() {
return getData(i, completions);
});
}(i));
}
return data;
})
.error(function(data) {
return data;
});
}]);
// loads individual page data, stores in pageData and sets up SCORM objectives from completable components (in completions service)
function getData(id, completions) {
return $.ajax({
url: 'json/page'+ (id + 1) +'.js',
dataType: 'json'
}).done(function(d) {
// push data into pageData array, for use later when pages are routed to
pageData.push(d);
// register this page's completable components in SCORM
completions.addPageComponents(id + 1, d);
}).fail(function() {
console.log('ERROR loading page index '+id);
});
}
I couldn't make sense of $q.all or promises, but a colleague pointed me to another SO answer that used something similar to the above.
When the 'content' factory is called, it calls the getData function to load the first .js file. Looping through up to the number of pages, I'm guessing the line...
tempData = tempData.then(...
...means that it waits for that load to complete before starting the next. The key thing for storing the currently loaded data is what happens in the getData.done function; The loaded data is pushed into pageData array for use later and the data is also passed into the completions service (to set up our objectives in SCORM).
All working as intended!
Using AngularJS and Typescript (Not to worry if you do not know TypeScript, an AngularJS proposal would do me) I am trying to create a directive for highcharts that has dynamic data that is populated by a call to a service that gets the chart data (via REST)
My html looks like this:
<div ng-repeat="request in controller.chartRequests">
<bar-chart id = "snapshotChart"
get-chart-data = "controller.getChartData(request)" // Some other attributes />
</bar-chart>
</div>
My hope is to dynamically draw the charts based on the number of stored requests I have in my controller.
The request object is used by the 'chartDataService' as shown below.
It returns the data as an array - eg: [1,12] and I resolve as a promise
public getChartData = (chartRequest) => {
let deferred = this.$q.defer();
this.chartDataService.dispatchCommand(chartRequest).then(
this.returnChartData // just returns a series
).then((series) => {
// This populates fine e.g [1,12]
deferred.resolve(series);
});
return deferred.promise;
};
constructor() {
let directive:ng.IDirective = {};
directive.restrict = 'E';
directive.replace = true;
directive.scope = {
getChartData: '&',
/** Other attributes **/
};
directive.link = (scope, element, attrs) => {
let highchartsOptions:HighchartsOptions = {
/**
* Some Highcharts config this all works fine
**/
series: [
{
data: scope.getChartData()
}
]
};
let chart = new Highcharts.Chart(
highchartsOptions
);
}
I have tried a scope.watch on getChartData() but this cause an infinite loop of requests it seems.
Not sure if anyone can help me out here - I may be approaching this problem in the wrong way.
Any help much appreciated!
Cheers
Can you help me please im new in angularJS,
i have a problem with Asynchrounous $http.get , i explain : i got data that i show in ngTable but in my html i got a empty table until i click to filter or to sort then i see my data.
i think its because i have a promise in my http.get. here my code to understand more ( sorry for my english)
'use strict';
(function() {
angular
.module('timeShareApp')
.controller('homeController', homeController);
function homeController($http, $scope, $filter, NgTableParams) {
$scope.users =[];
$http.get('/api/users/').success(function(response) {
$scope.users = response;
});
$scope.usersTable = new NgTableParams({
page: 1,
count: 10
}, {
total: $scope.users.length,
getData: function ($defer, params) {
$scope.data = params.sorting() ? $filter('orderBy')($scope.users, params.orderBy()) : $scope.users;
$scope.data = params.filter() ? $filter('filter')($scope.data, params.filter()) : $scope.data;
$scope.data = $scope.data.slice((params.page() - 1) * params.count(), params.page() * params.count());
$defer.resolve($scope.data);
}
});
}
homeController.$inject = ["$http", "$scope", "$filter", "NgTableParams"];
})();
for info : code works perfectly except that promise that i want to convert to synchonous if you can help me please.
Thank you in advance
In most cases, there is no reason to keep any data outside the ng-table's scope. My advice is to not modify or reference any scope variables because this can cause some pretty hard to track timing issues.
Have a look at the really good ng-table docs, which mostly have a working sample for your use case. See the docs here
Depending on the place where your filtering / sorting happens, you need to adapt this, but the following should basically work:
$scope.loadData = function() { return $http.get('/api/users/') };
$scope.usersTable = new NgTableParams(
{
page: 1,
count: 10
}, {
total: 0, // just the inital value - will be updated later on
getData: function ($defer, params) {
$scope.loadData().success(function (result) {
// assuming your result has a total and a data field...
// update the table params with the total amount of results
// could also be result.length...
params.total(result.total);
// sort and filter your data using the params object here
var data = result.data;
// give back the control to the table
$defer.resolve(data);
});
}
}
);
Please be aware to also set the params.total whenever your server responds. Otherwise the pagination will not be visible.
I think there is no problema on adding $scope.usersTable to me defined inside your promise resolve method. Have you tried that?
I have a object in mainController.js that is set as default as 99.
I am obtaining user location and do running some other function with it to calculate this value.
However, When I load the page, the page seems to load faster than this process. Therefore it displays 99 instead of the calculated value.
If I put console.log after the calculation, the object is successfully changed.
edit1:
status.success( function(data)
{
$scope.current = data;
$scope.$broadcast('back_end_connected');
});
$scope.getLocation = function()
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
var dirBuilt = false;
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
}
};
});
that "alert("aaa: "+ $scope.facilities[0].distance);" returns the value I want it to display but it is not displayed on the page....
(ng-bind would not work for some reason...)
How can I make the html wait for the operation? Thanks
You should not make HTML wait until you finish your Js code ! What you should be doing is showing a placeholder value - Loading image so that user know the page is loading some data.
Once you are done with your calculation, hide /replace the loading image with the data you want to show.
Quick example
Your view markup will have some HTML element to show the progress bar.And all your other contents will be in another div
<body ng-app="yourApp" ng-controller="yourCtrl as vm">
<div ng-show="loaderCount>0"> Loading something..</div>
<div ng-show="loaderCount==0">
<h4>{{userScore}}</h4>
</div>
</body>
And in your angular controller, You have a scope variable called loaderCount which you will increase everytime when you are doing some operation (http call/Long running function execution etc..). When you get your result, You decrease this variable value back. In your View You are hiding and showing the Loading Pane based on this value.
var yourApp= angular.module('yourApp', []);
var ctrl = function($scope, $http) {
var vm = this;
vm.loaderCount = 0;
vm.someValue = "";
vm.actionItems = [{ Name: "Test" }, { Name: "Test2" }];
vm.loaderCount++;
$http.get("../Home/GetSlowData").then(function(s) {
vm.loaderCount--;
vm.someValue = s.data;
});
};
yourApp.controller('yourCtrl', ['$scope', '$http', ctrl]);
This might not be the best angular code. But this will give you an idea about how to handle this use case. You should be using services to talk to Http endpoints instead of directly using $http in your angular controller.
Note that, $http.get returns a promise which allows you do things when you get the response from this asynchronous operation (the then event). You should make sure that your time taking calculation is returning a promise.
You can bind with the ngBind directive instead of {{}} and not write to the binded property when the calculation is done.
This will hide the result from the view while it is null.
Try to do something like this:
$scope.value = "Loading";
or
$scope.value = "";
$scope.calculate = function() {
$scope.value = yourcalculation;
}
$scope.calculate();
Or in your case i think if you use $scope.$apply() after you add data to your scope element will do the trick
Try this :
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
$scope.$apply(); // This should do the trick
}
};
});
Are you using a promise on the service? The function running your calculation can be in a separate angular factory which has .success and .error events to tap into. If the calculation is a success. You can pass the data back to your controller and then bind that data to the controller scope. I'll be at a computer soon and will add some code to explain further if needed.
This would be your distance calculator, I used a Factory over a service but you can read about why to use one over the other
A couple of things to keep in mind. You geolcation is more of a service then something to control since you're requesting from the user, you could add it to the below factory to expand the factories capabilities and allow you to use getLocation in other controllers.
make sure you add the distance service to your html document
also make sure you put the distance service in your controller and on the main angular app.
distanceService.js
(function(){
'use strict'
var distSrvc= angular.module("distanceService",[])
distSrvc.factory('DistanceCalc',['$http',function($http){
return{
getDistance:function(facilitiesData,locationData){
Object.keys(facilitiesData).length; // or if it's already an array of facilities use facilitiesData.length
// if its a javascript object and distance is already defined
for (var distance in facilitiesData) {
if (facilitiesData.hasOwnProperty(distance)) {
facilitiesData.distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
}
// if its an array of objects
for (var i = 0 ; i< facilitiesData.length;i++){
facilitiesData[i].distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
return facilitiesData
}
})();
Then in your controller you'll need to load the service for use.
yourcontroller.js this will give errors if you don't load it on the html page and add it to the main angular app.
(function(){
'use strict'
var myController= angular.module("distanceService",[])
myController.controller('myController',['$scope','DistanceCalc',function($http,DistanceCalc){ // see how i added the service and passed it to the function
$scope.getLocation = function(){
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
// this line below sends the data to the service
DistanceCalc.getDistance($scope.facilities,$scope.location)
.success(function(data){
//success i'm done running distance calculations and now i want to update the facilties object with the added distance
$scope.facilities = data
})
.error(function(data){
// some error occurred and data can tell you about that error
})
}
})();
I am trying to make a treeview control in angular js.
I have used the angular js directive( referred this link : angularjs treeview)
I have modified the above for having a big tree that contain tags.
My fiddle : fiddle sample
//angular module
var myApp = angular.module('myApp', ['angularTreeview']);
//test controller
myApp.controller('myController', function($scope){
$scope.clean_tree = function()
{
$scope.roleList = [];
};
$scope.show_tree = function () {
//debugger;
console.log("tag start : " + new Date());
//$scope.tagList = eval(abc);
$scope.roleList = $scope.roleList_tag;
$scope.$apply();
console.log("tag end : " + new Date());
};
The problem
The rendering of the tree gets slower and slower if I continuously follow this pattern:
click on : Tree Structure
click on : clean
Go To 1.
I dont understand why this is becoming slower and slower when i follow this pattern