I´m trying to dynamically create childControllers within a masterController. I got the idea from this page. Please see this JSFiddle and watch the console.
The + creates a new childController. Each childController has a random number, created in its controller function, and a number set by ng-init.
When I do the following:
click the +
click Emit
click BroadCast
the console output is:
new masterController
creating new childController..
new childController (undefined : 37818)
created the new childController
new childController (undefined : 49443)
child (0 : 49443) emitting..
masterController heard (0:49443)
broadcasting..
(undefined : 37818) received the broadcast
(0 : 49443) received the broadcast
done broadcasting
Here is becomes clear that two childControllers are generated, instead of just one. If I remove html-line 16
ng-controller="childController"
then only one controller is created, so I assume the problem is that this line creates another controller. How do I circumvent this, and am I correctly creating controllers dynamically?
You are correct in your assumption that ng-controller was creating another controller. To bypass this, you can pass it the controller constructor instead of instantiating it yourself.
Working fiddle: https://jsfiddle.net/acw9q0ya/
In createNewChildController:
var ControllerFn = function() {
return $controller('childController', { $scope: $scope.$new() }).constructor;
};
$scope.childControllers.push(ControllerFn)
Lastly in the ng-repeat, the name passed needs to match what was passed into $controller.
<div ...
ng-repeat="childController in childControllers"
ng-controller="childController"
...
>
ng-repeat creates the controller itself. There's no need for you to create the controller. Replace your code with this:
$scope.createNewChildController = function(){
console.log("\ncreating new childController..")
//var Controller = $controller('childController', { $scope: $scope.$new() });
$scope.childControllers.push({})
console.log("created the new childController")
}
If you want to use a dynamic controller like the example you want, keep in mind that you will have to have a variable for each item in the array:
$scope.SubController_1 = function() {
return $controller('ControllerName', { $scope: $scope.$new() }).constructor;
};
Related
I have the following situation in angular:
I load json data from a database on init() to a variable "data" and would like to use the variable "data" in a child controller (nested).
Because the loading from the database takes some time, the variable $scope.data in the child controller outputs as "undefined".
What is an elegant way to handle this situation? Do I need to use a promise on parent's init method inside the child controller? I would appreciate an example :).
// Parent Controller
app.controller('pCTRL', function($scope) {
$scope.init = function(id){}
//sets my variable $scope.data successfully via a rest API
//and for test, sets $scope.x to "blabla"
}
//Child Controller
app.controller('cCTRL', function($scope) {
console.log($scope.x); //outputs blabla properly
console.log($scope.data); // undefined
Thank you for your feedback!
A common solution is to use a watcher. For example:
app.controller('cCTRL', function($scope) {
$scope.$watch('data', function(newVal){
console.log(newVal); // outputs actual value
});
}
This is only needed if you need to have logic in the directive. If you just want to display data in the cCTRL template you don't need the above watcher, angular will update the template when the parent changes.
I am doing an app where when I click on a marker on a google.map triggers some action, specifically I want to get an object out of an array of objects to display the details of such object.
I use the controller defs like so
(function() {
'use strict';
angular
.module('gulpAngular')
.controller('GeolocationController', GeolocationController);
/** #ngInject */
function GeolocationController(HouseList) {
....
}
})();
The HouseList is a service defined elsewhere and having a method called getHouses()
Inside my controller, I do besides other things this:
var vm = this;
vm.houses = HouseList.getHouses();
Then I define my marker on the map like
allMarkers = new google.maps.Marker({....});
and add an Listener to it like below. To make things simple, I assign vm.house = vm.houses[0]
allMarkers.addListener('click', function() {
vm.house = vm.houses[0]
}
Now I suppose I should be able to use the vm.house object to display in my html block the attributes of this house object in the fashion of
<h4>{{vm.house.bedrooms}}</h4>
HOWEVER, NOTHING GETS DISPLAYED. I should see my vm.house object be updated and this reflected on the DOM, correct? What do I miss?
Funny: When i add a simple button on the html and use a ng-click function on it without anything other than say console.log(vm.house), not only it does display the correct object, but also the refresh of the html is happening. I am lost
thanks
Peter
addListener is not an Angular function and will not trigger the digest loop. You need to do it manually.
For example:
allMarkers.addListener('click', function() {
$scope.$apply(function () {
vm.house = vm.houses[0]
});
});
Note that you need to inject $scope for this.
ng-click triggers the digest loop, which is why using it will update the HTML.
I am writing my first non-trival Angular App and I have hit a snag with a directive. The directive takes data from a controller's scope and applies it to Google Chart. The chart is not the issue - which is to say it works fine with dummy data - it is access to the properties of the scope object which were obtained via http:
I am accessing data returned via an API in a service which utilizes $http:
dashboardServices.factory('SearchList', ['$http','$q',
function($http, $q){
return {
getSearchDetails:function(searchType, resultType){
return $http.get("api/searches/"+searchType+"/"+resultType)
.then(function(response){
if (typeof(response.data === 'object')) {
return response.data;
} else {
return $q.reject(response.data);
}
},function(response){
$q.reject(response.data);
});
}
}
}]);
In my controller, I am taking the response from this service and attaching to my scope via the promises' "then" method:
dashboardControllers.controller('DashboardCtrl', ['$scope', 'SearchList',
function($scope, SearchList){
$scope.searchData = {};
$scope.searchData.chartTitle="Search Result Performance"
SearchList.getSearchDetails("all", "count").then(function(response){
$scope.searchData.total = response.value; //value is the key from my API
});
SearchList.getSearchDetails("no_results", "count").then(function(response){
$scope.searchData.noResults = response.value;
});
}]);
To an extent this works fine, i can then use the 2-way binding to print out the values in the view AS TEXT. Note: I want to be able to write the values as text as I am trying to use a single scope object for both the chart and the textual data.
{{searchData.total | number}}
As mentioned, I have written a directive that will print a specific chart for this data, in this directive ONLY the $scope.searchData.chartTitle property is accessible. The values that were set in the then functions are not accessible in the directive's link method:
Here is the directive:
statsApp.directive('searchResultsPieChart', function(){
return{
restrict : "A",
scope:{
vals:'#vals'
},
link: function($scope, $elem, $attr){
var dt_data = $scope.vals;
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
}
});
Here is how I am using the directive in the view:
<div search-results-pie-chart vals="{{searchData}}"></div>
I can see that the issue is that the numeric values are not available to the directive despite being available when bound to the view.
Clearly the directive needs to be called later when these items are available or via some callback (or perhaps an entirely different approach), unfortunately i am not sure why this is the case or how to go about solving.
Any help would be greatly appreciated. I hope this makes sense.
I think the following will help you.
First change the directive scope binding for vals to use = instead of # (see this question for good explanation of the differences - basically # interpolates the value whereas = binds to the variable in the parent scope)
Then, move the part of the directive that creates the graph into a render function within your link function.
Then, $watch vals for any changes, then call the render function with the new values
You would also have to slightly change the approach of using ele[0], as you'll need to clear out the contents of it and add a new element with the new chart when the data changes (otherwise many charts will be added as the data changes!)
Here is an example of what to do in your link function with regard to the $watch and new render function (changing the $scope binding like I mentioned is not shown):
$scope.$watch('vals', function(newVals, oldVals) {
return $scope.render(newVals);
}, true);
$scope.render = function (dt_data) {
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
Hope this helps you out!!!
I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>
I have a scenario, i which I have a share button, I have a container controller, called metaCtrl, which is on the html tag.
And also inner controllers.
I have a share button, that calls the model.share() function on click.
code:
app.controller('metaCtrl', function($scope){
$scope.model = {};
$scope.model.share = function(){
$location.path('/share').search({'share' : '1'});
}
});
The controller of the share page it self:
app.controller('shareCtrl', function($scope)){
$scope.layout.shareVisible = $location.path().share == 1 ? true : false;
$scope.model.share = function(){
$scope.layout.shareVisible = $scope.layout.shareVisible ? false : true;
var shareUrlValue = $scope.layout.shareVisible ? 1 : null;
$location.search('share', shareUrlValue);
}
});
The idea is to use the same HTML pattern in the entire application, but only to toggle the share section on the share page(if the user is already there), and to send the user to the share view if he is not currently there.
The problem is that after I go to the sahre page, and then return to the other page, the function share() has a referance to the function in the shareCtrl, rather then to the metaCtrl.
I'm not sure there's enough information here, but I'll take a shot at it (I can't comment because I don't have enough rep). Notice that your shareCtrl is not creating a new model object, you're just assigning $scope.model.share = func....
That assignment is overriding the function in the parent controller because Angular is going up the scope chain to find the model object.
See this Plunker:
http://plnkr.co/edit/yv5rrpdlkniZdg94T4Lf
Notice with $scope.model = {} commented out in shareCtrl, the value of both fields is "shareCtrl". But, if you uncomment that line, then $scope.model is within the shareCtrl scope so Angular doesn't go up the scope chain looking.