I am having issues rendering Flotcharts with data populated from an AngularJS (v1.2.5) service call. When the data is hard-coded, the charts render as expected but not when assigned the value from a $resource call (console logging shows "undefined" as the value).
The odd thing is that the same variable I use to populate the chart data is displayed in the view and works fine which makes me think this could be some sort of race condition or scoping issue.
I have tried assigning the the $scope variables as a result of resolved promises without success to remedy race conditions as well as tried things such as $scope.$apply() from the chart directive.
Below are code excerpts from my application. Any help is appreciated. Thank you.
HTML wrapper:
<!DOCTYPE html>
<html data-ng-app="app" data-ng-controller="app">
<head>
<title>FlotChart Example</title>
</head>
<body>
<main data-ng-view></main>
</body>
</html>
Template from Overview route:
<div class="chart-title">{{getCount.count}} Total Transactions</div>
<div id="deposits" data-chart="pie" data-flot-chart data-ng-model="model" data-ng-controller="OverviewCtrl"></div>
Main Module ("app"):
;(function (angular) {
'use strict';
angular.module('app', ['ngRoute', 'ngResource', 'ngTouch', 'services.Localization', 'Login', 'Overview'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl'
})
.when('/overview', {
templateUrl: 'views/overview.html',
controller: 'OverviewCtrl'
})
.otherwise({
redirectTo: '/'
});
}])
.controller('App', ['$rootScope', 'Localize', function ($rootScope, Localize) {
// initialize localization
$rootScope.labels = Localize.get();
}]);
}(angular));
Overview Controller (used in charts and view):
;(function (angular) {
'use strict';
angular.module('Overview', ['services.DepositsSvc'])
.controller('OverviewCtrl', ['$rootScope', '$scope', 'DepositCount', 'DepositCountGET', function ($rootScope, $scope, DepositCount, DepositCountGET) {
$scope.getCount = DepositCount.get();
$scope.getGETCount = DepositCountGET.get();
$scope.model = {};
$scope.model.data = [
{
label: $rootScope.labels.gets,
data: $scope.getGETCount.count,
color: "#e4d672"
},
{
label: $rootScope.labels.puts,
data: $scope.getCount.count,
color: "#c2cfeb"
}
];
$scope.model.options = {
series: {
pie: {
show: true,
radius: 1,
label: {
radius: 2/3,
formatter: function (label, series) {
return '<div class="pie">' + label + ': ' +
series.data[0][1] + '<br>(' + Math.round(series.percent) + '%)</div>';
}
}
}
},
legend: {
show: false
}
};
}]);
}(angular));
Services (output from both calls is { count: number }):
;(function (angular) {
'use strict';
angular.module('services.DepositsSvc', ['ngResource', 'ngRoute'])
.factory('DepositCount', ['$resource', function ($resource) {
return $resource('/rest/deposits/count', {}, {
query: { method: 'GET', params: {}, isArray: true }
});
}])
.factory('DepositCountGET', ['$resource', function ($resource) {
return $resource('/rest/deposits/countgetdeposits', {}, {
query: { method: 'GET', params: {}, isArray: true }
});
}]);
}(angular));
Charts directive:
;(function (angular, $) {
'use strict';
angular.module('directives.FlotCharts', [])
.directive('flotChart', function () {
return {
restrict: 'EA',
controller: ['$scope', '$attrs', function ($scope, $attrs) {
var plotid = '#' + $attrs.id,
model = $scope[$attrs.ngModel];
$scope.$watch('model', function (x) {
$.plot(plotid, x.data, x.options);
});
}]
};
});
}(angular, jQuery));
You should try to distill this down to a smaller example that fits in a jsFiddle, so we can try it out.
One thing that I notice, though, is that you're doing a shallow watch, then setting model.data and model.options. So unless I'm missing something, the watch will not fire when those change; only when model itself changes. Try passing true as the third argument to watch.
I'm not sure that would solve your problem (because this maybe the result of numerous problem) but I think you got the directive scope and model attachment wrong:
*. I'm not sure why you encapsulate the directive as a self invoking function (never saw this style before, and Sure it isn't needed.
*. you are creating a new module here with the ('directives.flotCharts',[]) syntax, without the [] you could attach the directive to any existing module. the important part is that this module isn't injected into app! you should include it in the app module dependency array. without it, angular doesn't know about this directive (and also include the js file in index.html... I sometimes forget and wonder why is this not working)
*. I would suggest rewriting the directive as (and notice the comments also):
something like this:
angular.module('directives.FlotCharts', []) // creaating a new module here
.directive('flotChart', function () {
return {o
restrict: 'EA',
scope:{model:"=ngModel"},//creating a two ways binding to ngModel
controller: ['$scope', '$attrs','$element', function ($scope, $attrs,$element) {
var plotid = '#' + $attrs.id, // you are doing an ugly hack instead of using $element, which comes with the directive, $element is a jquery (or jquery lite) object (if you included jQuery **before** angular.js in index.html it is a jQuery object)
$scope.$watch(function(){return $scope.model}, function (x) {
if (!x){
return;
}
$.plot(plotid, x.data, x.options);//may need to wrap this in an $apply, depends.. //surly this should be called on the $element object - something like: $element.plot(x.data, x.options) although I don't now the specifics of this lib.
}, true); //the true is needed for a deep equality and not just shallow check, which sometimes has problems with objects.. not sure you need this here.
}]
};
});
I also suspect you are treating promises wrong. but not sure this is the problem here (although when everything else would work this probabely could and would be a problem. to scope this answer and question I think you should refine the promises issue to a different and specific question.
Good luck!
Solution documented at AngularJS Google Group.
Related
Based on a comment of another question from me I tried to create a directive to reduce my code. Here what I got:
Directive (very small for testing. Later it will be more elements):
BebuApp.directive('inputText', function(){
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input type="text" ng-model="model" />'
}
});
State:
.state('app', {
abstract: true,
url: '',
templateUrl: 'layout.html',
resolve: {
authorize: function ($http) {
return $http.post(API.URL_PING);
}
}
})
.state('app.application-detail', {
url: "/bewerbungen/{id}",
templateUrl: "views/application-detail/application-detail.html",
data: {pageTitle: 'Meine Bewerbungen', pageSubTitle: ''},
controller: "ApplicationDetailController",
resolve: {
prm: function ($http, $stateParams) {
// $http returns a promise for the url data
return $http.get(API.URL_JOBAPPLICATION_GET_DETAILS + "/" + $stateParams.id);
}
}
})
Controller:
'use strict';
BebuApp.controller('ApplicationDetailController', function($rootScope, $scope, $http, $stateParams, API, prm) {
$scope.jobApplication = prm.data;
console.log(prm);
$scope.$on('$viewContentLoaded', function() {
// initialize core components
App.initAjax();
});
});
Template / View:
<div class="margin-top-10">
{{ jobApplication }}
<input-text model="jobApplication.description"></input-text>
</div>
When the page is loaded I can see the correct model (output by {{jobApplication}}), but the input field is empty. I need a normal two way binding. When the model changes in the scope it should also change in the directive and vice versa. As far as I understand the model is retrieved by the resolve callback / function in the state, so it should be "there" when the template is compiled.
Where is my problem?
I found the problem after an closer look to the model (thanks to the comments!). In fact the model I received from my backend was a collection with just one entry. It looked like this:
[{id:"xxx", description:"test".....}]
Of course it must look like this:
{id:"xxx", description:"test"...}
After fixing this stupid mistake, everything works fine!
I have index.html main file and partial view1.html and I use ng-route. So I want controller view1.js to be loaded only when view1.html is accessed. So I successfully loaded the file using resolve directive. Since javascript loads in background angular starts to parse view1.html and I get:
[ng:areq] Argument 'SimpleController2' is not a function, got undefined
How do I to tell angular that a new controller has been added, and it should parse view1.html only after applying the controller.
Check this out
main.js
var demoApp = angular.module('demoApp', ['ngRoute']);
demoApp.config(function ($routeProvider) {
$routeProvider
.when('/view1', {
controller: 'SimpleController1',
templateUrl: 'View1.html',
resolve: {
lol : function get() {
console.log('file is being loaded');
var fileRef = document.createElement('script');
fileRef.setAttribute("type", "text/javascript");
fileRef.setAttribute("src", "view1.js");
document.getElementsByTagName("head")[0].appendChild(fileRef);
fileRef.onload = function () {
console.log('file loaded');
}
}
}
})
.when('/view2',
{
controller: 'SimpleController',
templateUrl: 'View2.html'
})
.otherwise({redirectTo: '/view1'});
});
console.log('config has been added');
controller.js
console.log("file parsed");
demoApp.filter('myFilter', function ($sce) {
return function (input, isRaw) {
input = input.replace(/a/g, 'b');
return $sce.trustAsHtml(input);
};
});
demoApp.controller("SimpleController1", function ($scope, simpleFactory) {
$scope.names = [
{name: 'Nick', city: 'London'},
{name: 'Sick', city: 'Tokio'},
{name: 'Brick', city: 'cellar'}
];
});
I'm new to angular I tried this, this (how to apply files)
and this (gave up with applying $controllerProvider)
this (didn't succeed either) and this (couldn't make the whole thing work together) I'm not even sure that those described my needs. Please give me a clue what post I should dive deeper.
You can't check the existence of the controller without dry-running it with $controller and catching an exception, which is terrible thing to do.
Even then you will have hard time to register a new controller, because after config phase app.controller can't be used, and $controllerProvider.register is available only in config blocks.
You may look at existing solutions for lazy loading, i.e. ocLazyLoad. Here is how it manages routes.
I'm trying to write a generalized route in my application and resolve the view and controller names on the fly based on the route params.
I have the following code that works:
$stateProvider.state('default', angularAMD.route({
url: '/:module/:action?id',
templateUrl: function (params) {
var module = params.module;
var action = module + params.action.charAt(0).toUpperCase()
+ params.action.substr(1);
return 'app/views/' + module + '/' + action + 'View.html';
},
controller: 'userController',
}));
However, I'm unable to figure out a way to resolve the controller name dynamically. I tried using resolve as described here, but ui-router seems to handle resolve differently than angular-route.
Any pointers?
EDIT: I've already tried using controllerProvider but it doesn't work for me (for instance, the following code just returns a hard coded controller name to test whether it actually works):
controllerProvider: function () {
return 'userController';
}
Gives me the following error:
Error: [ng:areq] Argument 'userController' is not a function, got undefined
http://errors.angularjs.org/1.3.3/ng/areq?p0=userController&p1=not%20aNaNunction%2C%20got%20undefined
This is a link to working plunker.
solution
We need two features of the UI-Router:
resolve (to load the missing pieces of js code)
controllerProvider (see cites from documentation below)
angularAMD - main.js definition
This would be our main.js, which contains smart conversion controllerName - controllerPath:
require.config({
//baseUrl: "js/scripts",
baseUrl: "",
// alias libraries paths
paths: {
"angular": "angular",
"ui-router": "angular-ui-router",
"angularAMD": "angularAMD",
"DefaultCtrl": "Controller_Default",
"OtherCtrl": "Controller_Other",
},
shim: {
"angularAMD": ["angular"],
"ui-router": ["angular"],
},
deps: ['app']
});
controllers:
// Controller_Default.js
define(['app'], function (app) {
app.controller('DefaultCtrl', function ($scope) {
$scope.title = "from default";
});
});
// Controller_Other.js
define(['app'], function (app) {
app.controller('OtherCtrl', function ($scope) {
$scope.title = "from other";
});
});
app.js
Firstly we would need some method converting the param (e.g. id) into controller name. For our test purposes let's use this naive implementation:
var controllerNameByParams = function($stateParams)
{
// naive example of dynamic controller name mining
// from incoming state params
var controller = "OtherCtrl";
if ($stateParams.id === 1) {
controller = "DefaultCtrl";
}
return controller;
}
.state()
And that would be finally our state definition
$stateProvider
.state("default", angularAMD.route({
url: "/{id:int}",
templateProvider: function($stateParams)
{
if ($stateParams.id === 1)
{
return "<div>ONE - Hallo {{title}}</div>";
}
return "<div>TWO - Hallo {{title}}</div>";
},
resolve: {
loadController: ['$q', '$stateParams',
function ($q, $stateParams)
{
// get the controller name === here as a path to Controller_Name.js
// which is set in main.js path {}
var controllerName = controllerNameByParams($stateParams);
var deferred = $q.defer();
require([controllerName], function () { deferred.resolve(); });
return deferred.promise;
}]
},
controllerProvider: function ($stateParams)
{
// get the controller name === here as a dynamic controller Name
var controllerName = controllerNameByParams($stateParams);
return controllerName;
},
}));
Check it here, in this working example
documentation
As documented here: $stateProvider, for a state(name, stateConfig) we can use controller and controllerProvider. Some extract from documentation:
controllerProvider
...
controller (optional) stringfunction
Controller fn that should be associated with newly related scope or the name of a registered controller if passed as a string. Optionally, the ControllerAs may be declared here.
controller: "MyRegisteredController"
controller:
"MyRegisteredController as fooCtrl"}
controller: function($scope, MyService) {
$scope.data = MyService.getData(); }
controllerProvider (optional) function
Injectable provider function that returns the actual controller or string.
controllerProvider:
function(MyResolveData) {
if (MyResolveData.foo)
return "FooCtrl"
else if (MyResolveData.bar)
return "BarCtrl";
else return function($scope) {
$scope.baz = "Qux";
}
}
...
resolve
resolve (optional) object
An optional map<string, function> of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them ALL to be resolved before the controller is instantiated...
I.e. let's use controllerProvider:
... to resolve the controller name dynamically...
In case, that you managed to get here, maybe you'd like to check another similar solution with RequireJS - angular-ui-router with requirejs, lazy loading of controller
I'm trying to get to work angular.js, ui-router, and require.js and feel quite confused. I tried to follow this tutorial http://ify.io/lazy-loading-in-angularjs/. First, let me show you my code:
app.js =>
var app = angular.module('myApp', []);
app.config(function ($stateProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
$stateProvider.state('home',
{
templateUrl: 'tmpl/home-template.html',
url: '/',
controller: 'registration'
resolve: {
deps: function ($q, $rootScope) {
var deferred = $q.defer(),
dependencies = ["registration"];
require(dependencies, function () {
$rootScope.$apply(function () {
deferred.resolve();
});
})
return deferred.$promise;
}
}
}
);
app.lazy = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
});
Now in my registration.js I have following code:
define(["app"], function (app) {
app.lazy.controller("registration" , ["$scope", function ($scope) {
// The code here never runs
$scope.message = "hello world!";
}]);
});
everything works well, even the code in registration.js is run. but the problem is code inside controller function is never run and I get the error
Error: [ng:areq] http://errors.angularjs.org/1.2.23/ng/areq?p0=registration&p1=not a function, got undefined
Which seems my code does not register controller function successfully. Any Ideas?
P.s. In ui-router docs it is said "If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $routeChangeSuccess event is fired." But if I put the deferred.resolve(); from mentioned code inside a timeOut and run it after say 5 seconds, my controller code is run and my view is rendered before resolve, Strange.
Seems like I ran into the exact same problem, following the exact same tutorial that you did, but using ui-router. The solution for me was to:
Make sure the app.controllerProvider was available to lazy controller script. It looked like you did this using app.lazy {...}, which a really nice touch BTW :)
Make sure the lazy ctrl script uses define() and not require() I couldn't tell from your code if you had done this.
Here is my ui-router setup with the public app.controllerProvider method:
app.config(function ($stateProvider, $controllerProvider, $filterProvider, $provide, $urlRouterProvider) {
app.lazy = {
controller: $controllerProvider.register,
directive: $compileProvider.directive,
filter: $filterProvider.register,
factory: $provide.factory,
service: $provide.service
};
$urlRouterProvider.otherwise('/');
$stateProvider
.state('app', {
url:'/',
})
.state('app.view-a', {
views: {
'page#': {
templateUrl: 'view-a.tmpl.html',
controller: 'ViewACtrl',
resolve: {
deps: function ($q, $rootScope) {
var deferred = $q.defer();
var dependencies = [
'view-a.ctrl',
];
require(dependencies, function() {
$rootScope.$apply(function() {
deferred.resolve();
});
});
return deferred.promise;
}
}
}
}
});
});
Then in my lazy loaded controller I noticed that I had to use require(['app']), like this:
define(['app'], function (app) {
return app.lazy.controller('ViewACtrl', function($scope){
$scope.somethingcool = 'Cool!';
});
});
Source on GitHub: https://github.com/F1LT3R/angular-lazy-load
Demo on Plunker: http://plnkr.co/edit/XU7MIXGAnU3kd6CITWE7
Changing you're state's url to '' should do the trick. '' is root example.com/, '/' is example.com/#/.
I came to give my 2 cents. I saw you already resolved it, I just want to add a comment if someone else have a similar problem.
I was having a very similar issue, but I had part of my code waiting for the DOM to load, so I just called it directly (not using the "$(document).ready") and it worked.
$(document).ready(function() { /*function was being called here*/ });
And that solved my issue. Probably a different situation tho but I was having the same error.
Could you help me to understand how to load controller in the example below before the view? It looks like the view is loaded just immediately while the controller is not loaded yet.
//app.js
$stateProvider.state('index', {
url: "/",
views: {
"topMenu": {
templateUrl: "/Home/TopMenu",
controller: function($scope, $injector) {
require(['controllers/top-menu-controller'], function(module) {
$injector.invoke(module, this, { '$scope': $scope });
});
}
}
}
});
//top-menu-controller.js
define(['app'], function (app) {
app.controller('TopMenuCtrl', ['$scope', function ($scope) {
$scope.message = "It works";
}]);
});
//Home/TopMenu
<h3>TopMenu</h3>
<div ng-controller="TopMenuCtrl">
{{message}}
</div>
I created working plunker here.
Let's have this index.html:
<!DOCTYPE html>
<html>
<head>
<title>my lazy</title>
</head>
<body ng-app="app">
#/home // we have three states - 'home' is NOT lazy
#/ - index // 'index' is lazy, with two views
#/other // 'other' is lazy with unnamed view
<div data-ui-view="topMenu"></div>
<div data-ui-view=""></div>
<script src="angular.js"></script> // standard angular
<script src="angular-ui-router.js"></script> // and ui-router scritps
<script src="script.js"></script> // our application
<script data-main="main.js" // lazy dependencies
src="require.js"></script>
</body>
</html>
Let's observe the main.js - the RequireJS config:
require.config({
//baseUrl: "js/scripts",
baseUrl: "",
// alias libraries paths
paths: {
// here we define path to NAMES
// to make controllers and their lazy-file-names independent
"TopMenuCtrl": "Controller_TopMenu",
"ContentCtrl": "Controller_Content",
"OtherCtrl" : "Controller_Other",
},
deps: ['app']
});
In fact, we only create aliases (paths) for our ControllerNames - and their Controller_Scripts.js files. That's it. Also, we return to require the app, but we will in our case use different feature later - to register lazily loaded controllers.
what does the deps: ['app'] mean? Firstly, we need to provide file app.js (the 'app' means find app.js) :
define([], function() {
var app = angular.module('app');
return app;
})
this returned value is the one we can ask for in every async loaded file
define(['app'], function (app) {
// here we would have access to the module("app")
});
How will we load controllers lazily? As already proven here for ngRoute
angularAMD v0.2.1
angularAMD is an utility that facilitates the use of RequireJS in AngularJS applications supporting on-demand loading of 3rd party modules such as angular-ui.
We will ask angular for a reference to $controllerProvider - and use it later, to register controllers.
This is the first part of our script.js:
// I. the application
var app = angular.module('app', [
"ui.router"
]);
// II. cached $controllerProvider
var app_cached_providers = {};
app.config(['$controllerProvider',
function(controllerProvider) {
app_cached_providers.$controllerProvider = controllerProvider;
}
]);
As we can see, we just created the application 'app' and also, created holder app_cached_providers (following the angularAMD style). In the config phase, we ask angular for $controllerProvider and keep reference for it.
Now let's continue in script.js:
// III. inline dependency expression
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$urlRouterProvider
.otherwise("/home");
$stateProvider
.state("home", {
url: "/home",
template: "<div>this is home - not lazily loaded</div>"
});
$stateProvider
.state("other", {
url: "/other",
template: "<div>The message from ctrl: {{message}}</div>",
controller: "OtherCtrl",
resolve: {
loadOtherCtrl: ["$q", function($q) {
var deferred = $q.defer();
require(["OtherCtrl"], function() { deferred.resolve(); });
return deferred.promise;
}],
},
});
}
]);
This part above shows two states declaration. One of them - 'home' is standard none lazy one. It's controller is implicit, but standard could be used.
The second is state named "other" which does target unnamed view ui-view="". And here we can firstly see, the lazy load. Inside of the resolve (see:)
Resolve
You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.
If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $stateChangeSuccess event is fired.
With that in our suite, we know, that the controller (by its name) will be searched in angular repository once the resolve is finished:
// this controller name will be searched - only once the resolve is finished
controller: "OtherCtrl",
// let's ask RequireJS
resolve: {
loadOtherCtrl: ["$q", function($q) {
// wee need $q to wait
var deferred = $q.defer();
// and make it resolved once require will load the file
require(["OtherCtrl"], function() { deferred.resolve(); });
return deferred.promise;
}],
},
Good, now, as mentioned above, the main contains this alias def
// alias libraries paths
paths: {
...
"OtherCtrl" : "Controller_Other",
And that means, that the file "Controller_Other.js" will be searched and loaded. This is its content which does the magic. The most important here is use of previously cached reference to $controllerProvider
// content of the "Controller_Other.js"
define(['app'], function (app) {
// the Default Controller
// is added into the 'app' module
// lazily, and only once
app_cached_providers
.$controllerProvider
.register('OtherCtrl', function ($scope) {
$scope.message = "OtherCtrl";
});
});
the trick is not to use app.controller() but
$controllerProvider.Register
The $controller service is used by Angular to create new controllers. This provider allows controller registration via the register() method.
Finally there is another state definition, with more narrowed resolve... a try to make it more readable:
// IV ... build the object with helper functions
// then assign to state provider
var loadController = function(controllerName) {
return ["$q", function($q) {
var deferred = $q.defer();
require([controllerName], function() {deferred.resolve(); });
return deferred.promise;
}];
}
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
var index = {
url: "/",
views: {
"topMenu": {
template: "<div>The message from ctrl: {{message}}</div>",
controller: "TopMenuCtrl",
},
"": {
template: "<div>The message from ctrl: {{message}}</div>",
controller: "ContentCtrl",
},
},
resolve : { },
};
index.resolve.loadTopMenuCtrl = loadController("TopMenuCtrl");
index.resolve.loadContentCtrl = loadController("ContentCtrl");
$stateProvider
.state("index", index);
}]);
Above we can see, that we resolve two controllers for both/all named views of that state
That's it. Each controller defined here
paths: {
"TopMenuCtrl": "Controller_TopMenu",
"ContentCtrl": "Controller_Content",
"OtherCtrl" : "Controller_Other",
...
},
will be loaded via resolve and $controllerProvider - via RequireJS - lazily. Check that all here
Similar Q & A: AngularAMD + ui-router + dynamic controller name?
On one project I used lazy loading of controllers and had to manually call a $digest on the scope to have it working. I guess that this behavior does not change with ui-router.
Did you try that ?
define(['app'], function (app) {
app.controller('TopMenuCtrl', ['$scope', function ($scope) {
$scope.message = "It works";
$scope.$digest();
}]);
});