AngularJS: Pass functions and variables to directive's - angularjs

I have one situation where I have to pass the directive, functions and variables declared in the controller. I can easily do this with $scope. But I read one article which states that we should not populate the scope instead use this. The article had the following example -
//Don't do this
app.controller('MyCtrl', function($scope){
$scope.name = 'Techno Fattie';
});
//Do this
var MyCtrl = function(){
this.name = 'Techno Fattie';
};
app.controller('MyCtrl', MyCtrl);
I liked the idea and I tried implementing the same in my situation which is as follows -
I have a CountryController - which I modified to use this instead of $scope.
I have a countryList.tpl.html - which has only a directive in it and no other code.
Now I have a parent controller which has a stateProvider where I have configuration for country. something like this -
.state('app.country', {
url: "/country",
templateUrl: "countryList.tpl.html",
controller: CountryController
})
I converted the controller's $scope to this but then I observed that I the directive is not receiving the function and variables and the page is not loading properly.
Is it that if the tpl file has a directive then this approach is not useful? If that is not true, then how can we do this? Can anyone help?

Related

ui-router controller stateparams

I have the following usecase:
I have two views which are identical, as well as two controllers which are very similar. I intend to extend one controller with the other and just override the diff. The problem I am having is in ui-router I want to choose a controller by name (Since the templates is shared, and the controllers differ this cannot be declared in the template itself)
Before this refactoring I had the following code:
.state('edit-page-menu', {
url: '/sites/:siteId/page_menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: ['$scope', '$stateParams', (scope, stateParams) => {
scope.siteId = stateParams.siteId
scope.menuId = stateParams.menuId
}]
})
Which adds the stateParams to the scope. In this usecase I had the controller defined in the template and could modify the scope that way. However now when I switch to controller by name approach
.state('edit-page-menu', {
url: '/sites/:siteId/menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: 'editMenuCtrl'
})
I don't know how to inject / add the $stateParams to the controller. And I would really like to avoid injecting "$state" in the controller and fetch the parameters that way. Is there a way to modify the scope of a controller if you choose it by name?
Best regards.

Angular: injecting state into controllers (or binding "models" to controllers)

Suppose I have a general purpose controller, TableController, that can be used in multiple places in the app to display a table of Key x Value pairs via a custom directive, ui-table, that generates an HTML table.
angular.module('ui.table', [])
.controller('TableController', ['$scope', 'data',
function($scope, data) { $scope.data = data; }])
.directive('uiTable', function() {
return { templateUrl: 'table.html' }
});
I could use the controller in the following template:
<div ng:controller="TableController">
<div ui-table></div>
</div>
And create a factory to pass data to this controller.
.factory('data', function() {
return [{'App':'Demo', 'Version':'0.0.1'}];
})
But, I have multiple controllers (sometimes in the same views), so I need to "bind" a particular factory to a particular controller (e.g., UserProfile, AppData, etc.)
I have started to look at angular-ui-router's $stateProvider, but it seems too complicated for what must be a typical use case? What I'd really like to be able to do is use the template to annotate which factory (what I think of as a model) should be used for that particular controller. E.g., something like:
<div ng:controller="TableController" ng:model="AppData"></div>
What is the right approach?
EDIT:
I've figured out $stateProvider and "resolve" allows me to map provider services onto injected values for the state's main controller -- but the controller I want to influence is a child of this controller.
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home/view.html',
controller: 'MainViewController',
resolve: {
'data': 'AppData'
}
});
So, I still can't figure out how to influence the controllers inside the state's view.
I think what you are looking for is simply passing your data into the directive through attributes. Then use an isolated scope in directive so you can have multiple instances active at the same time
<div ng-controller="ViewController">
<div ui-table dataSource="tableData"></div>
</div>
Then your directive would be written in a generic way to be re-usable regardless of the data passed in.
.factory('SomeService', function(){
var data ={
headings: ['ID','Age','Name'],
rows:[
{id:1, 33,'Bill'}......
]
};
return {
get:function(){ return data;}
};
})
.controller('ViewController', function($scope, SomeService){
$scope.tableData = SomeService.get();
})
.directive.directive('uiTable', function () {
return {
scope: {
dataSource: '=' // isolated scope, 2 way binding
}
templateUrl: 'table.html',
controller: 'TableController', // this controller can be injected in children directives using `require`
}
});
In essence this is just reversing your layout of controller/directive. Instead of TableController wrapping the directive, it is used internally within directive. The only reason it is a controller in the directive is to allow it to be require'd by child directives such as perhaps row directive or headings directive and even cell directive. Otherwise if not needing to expose it for injection you can use link and put all sorts of table specific operations in there
As mentioned in my comments there are various approaches to creating a table directive. One is with heavy configuration objects, the other is with a lots of declarative view html that use many child directives. I would suggest analyzing the source of several different grid/table modules to see what best suits your coding style
Thanks in part to #charlietfl (above) I have an answer:
<ui-graph model="SomeGraphModel"></ui-graph>
Then:
angular.module('ui.graph', [])
.directive('uiGraph', [function() {
return {
controller: 'GraphController',
scope: {
model: '#model' // Bind the element's attribute to the scope.
}
}
}]);
.controller('GraphController', ['$injector', '$scope', '$element',
function($injector, $scope, $element) {
// Use the directive's attribute to inject the model.
var model = $scope.model && $injector.get($scope.model);
$scope.graph = new GraphControl($element).setModel(model);
}])
Then somewhere else in the app (i.e., not necessarily in the directive/controller's module):
angular.module('main', [])
.factory('SomeGraphModel', function() {
return new GraphModel();
})

Angular's "controllerAs" not working in routeProvider

I am trying to use the controllerAs property on a $routeProvider route without any success.
Here is the sample code:
var app = angular.module('app', ['ngRoute']);
app.config(['$routeProvider', '$locationProvider',
function($routeProvider) {
$routeProvider
.when('/', {
template:'<div>This should be visible:{{ ctrl.one }}</div><div>This should not:{{ one }}</div>',
controller: 'Ctrl',
controllerAs: 'ctrl',
});
}]);
app.controller('Ctrl', function($scope) {
$scope.one = 'actual';
});
Not sure if this is a bug or I am doing something wrong, This is a plnkr that demonstrates the issue
Actual problem:
You seem to be using controllerAs (assigning a value of 'ctrl') originally, but then later not making use of it in the rest of your code. (you used $scope)
Solution:
Working demo as per your sample
When I've been using the controllerAs syntax you need to use one of the following pattern(s) to get access to the instance of the controller:
As opposed to appending the property to $scope, you have to bind to the controller scope. Note this is different to $scope. For lack of a better term, you need to bind controller itself (think of it as its context). As we're dealing with the display layer or view model, I tend to use var vm = this; as a convention, but this personal preference.
[A]: Preferred Solution
app.controller('Ctrl', function() {
this.one = 'actual';
});
//or using 'vm' convention
app.controller('Ctrl', function() {
var vm = this;
vm.one = 'actual';
});
[B]
app.controller('Ctrl', function() {
var vm = {};
vm.one = 'actual';
return vm;
});
Explanation:
When I first started using Angular, [B] was the approach that I used, purely coming from a Knockout background. I was used to binding to a "container" object then binding the object to the view. That being said, I prefer to use [A], or append directly to $scope and forgo the alias completely. Reasons:
I feel its cleaner ITO readability
As #Swordfish0321 stated, [A] is more performant (should it be a concern to you)
I had binding issues with custom directives I wrote that where dependent on certain parent scope properties (specific to my code-base)
Just as a visual:
Demo
app.controller('Ctrl', function($scope) {
var vm = this;
vm.one = 'actual';
console.log($scope)
});
Passing in the $scope object and further inspecting it, you'll see a new ctrl child object containing all your public properties and functions that was bound to vm inside the controller code. This is because you've assigned var vm = this. Meaning the vm object in the code is referencing the controller's own scope, which ultimately gets bound to the view. controllerAs basically groups all properties and functions contained internal to the controller to a new object named after the alias that you've provided.
To be clear--because I don't think the accepted answer was explicit--the problem with your example is that even though you are assigning a value to controllerAs you are bypassing it by still using $scope.
The "vm" approach stands for view-model which is just a convention, but IMO is far more representative of what is actually going on than "$scope". All we're really trying to do here is bind the view to the view model.
That being said you can technically use both controllerAs AND normal $scope at the same time plunk.
Additionally, the difference between Rohan's examples A and B is that A is the way you should be doing it because you are able to leverage JavaScript's prototypal inheritance which is conducive to better perf. It's also worth noting that because you are now using controllerAs and this you no longer need to inject $scope.
// Functioning Example
var app = angular.module('app', ['ngRoute']);
app.config(['$routeProvider', '$locationProvider',
function($routeProvider) {
$routeProvider
.when('/', {
template:'<div>This should be visible:{{ vm.one }}</div><div>This should not:{{ one }}</div>',
controller: 'Ctrl',
controllerAs: 'vm',
});
}]);
app.controller('Ctrl', function() {
this.one = 'actual';
});
The controllerAs is an alias for the controller instance, if you would like to use that, you have to store data into the controller instance this instead of $scope:
app.controller('Ctrl', function($scope) {
var ctrl = this; // assign to a variable to be consistent when using in the template
ctrl.one = 'actual';
});
Example Plunker: http://plnkr.co/edit/r8AYtSKbiLQAaPBPriRp?p=preview
If you work with $scope, you will have to return $scope at the end of the function for the controllerAs syntax to function.
app.controller('Ctrl', function($scope) {
$scope.one = 'actual';
// return $scope
return $scope;
});
Good Luck.

Dynamically loading controllers with their own scopes

I'm having a problem which I'm not sure whether is a down to a limitation of Angular (possibly) or a limitation of my knowledge of Angular (probably).
I am trying to take an array of controllers, and dynamically create/load them. I have a prototype working to the point where the controllers run and the root scope can be accessed, but I cannot dynamically attach ng-controller to divs in order to encapsulate the controllers into their own local scopes.
The problem is that the templates are bound to the root scope but not to their own scopes.
My example will hopefully explain my quandary better.
JSFiddle: http://jsfiddle.net/PT5BG/22/ (last update 16:30 BST)
It may not make sense why I am doing it this way, but I have pulled this concept out of a larger system I am creating. In case you have other suggestions, these are the laws by which I am bound:
Controllers cannot be hard-coded, they must be built from an array
Scopes cannot be shared between controllers, they must have their own scopes
The docs on AngularJS are not exactly comprehensive so I'm hoping someone here can help!
You can just pass the controller name through and use the $controller service and pass the locals through to it. You'll need some sort of ModuleCtrl thing to co-ordinate all this. Here is a basic example that does what you want.
http://jsfiddle.net/PT5BG/62/
angular.module('app', [])
.controller('AppCtrl', function ($scope, $controller) {
$scope.modules = [
{ name: "Foo", controller: "FooCtrl" },
{ name: "Bar", controller: "BarCtrl" }]
})
.controller('ModuleCtrl', function ($scope, $rootScope, $controller) {
$controller($scope.module.controller, { $rootScope: $rootScope, $scope: $scope });
})
.controller('FooCtrl', function ($rootScope, $scope) {
$rootScope.rootMessage = "I am foo";
$scope.localMessage = "I am foo";
console.log("Foo here");
})
.controller('BarCtrl', function ($rootScope, $scope) {
$rootScope.rootMessage = "I am bar";
$scope.localMessage = "I am bar";
console.log("Bar here");
});
The way I finally got around this was quite simple, it was just a case of working it out.
So I have a list of modules, that I get from an API, and I want to instantiate them. I include the template file by building the path via convention, like so:
<!-- the ng-repeat part of the code -->
<div ng-repeat="module in modules">
<ng-include src="module.name + '.tpl.html'"></ng-include>
</div>
In each of the modules template files, I then declare the ng-controller and I declare a method to fire in ng-init. As the template is still within the ng-repeat loop, it has access to module, which has the data we want to pass to the child controller. ng-init runs on the local scope, so we pass in the module object:
<!-- the template of the module -->
<div ng-controller="ModuleCtrl" ng-init="init(module)">
...
</div>
And then we store it on the local scope and there you go, injected the object.
/* the controller of the module */
.controller('ModuleCtrl', function ($scope) {
$scope.init = function(module) {
this.module = module;
};
// this.module is now available inside the controller
});
It took a bit of hacking but it works perfectly for now.

$routeParams is empty in main controller

I have this piece of layout html:
<body ng-controller="MainController">
<div id="terminal"></div>
<div ng-view></div>
<!-- including scripts -->
</body>
Now apparently, when I try to use $routeParams in MainController, it's always empty. It's important to note that MainController is supposed to be in effect in every possible route; therefore I'm not defining it in my app.js. I mean, I'm not defining it here:
$routeProvider.when("/view1", {
templateUrl: "partials/partial1.html"
controller: "MyCtrl1"
})
$routeProvider.when("/view2", {
templateUrl: "partials/partial2.html"
controller: "MyCtrl2"
})
// I'm not defining MainController here!!
In fact, I think my problem is perfectly the same as this one: https://groups.google.com/forum/#!topic/angular/ib2wHQozeNE
However, I still don't get how to get route parameters in my main controller...
EDIT:
What I meant was that I'm not associating my MainController with any specific route. It's defined; and it's the parent controller of all other controllers. What I'm trying to know is that when you go to a URL like /whatever, which is matched by a route like /:whatever, why is it that only the sub-controller is able to access the route parameter, whereas the main controller is not? How do I get the :whatever route parameter in my main controller?
The $routeParams service is populated asynchronously. This means it will typically appear empty when first used in a controller.
To be notified when $routeParams has been populated, subscribe to the $routeChangeSuccess event on the $scope. (If you're in a component that doesn't have access to a child $scope, e.g., a service or a factory, you can inject and use $rootScope instead.)
module.controller('FooCtrl', function($scope, $routeParams) {
$scope.$on('$routeChangeSuccess', function() {
// $routeParams should be populated here
});
);
Controllers used by a route, or within a template included by a route, will have immediate access to the fully-populated $routeParams because ng-view waits for the $routeChangeSuccess event before continuing. (It has to wait, since it needs the route information in order to decide which template/controller to even load.)
If you know your controller will be used inside of ng-view, you won't need to wait for the routing event. If you know your controller will not, you will. If you're not sure, you'll have to explicitly allow for both possibilities. Subscribing to $routeChangeSuccess will not be enough; you will only see the event if $routeParams wasn't already populated:
module.controller('FooCtrl', function($scope, $routeParams) {
// $routeParams will already be populated
// here if this controller is used within ng-view
$scope.$on('$routeChangeSuccess', function() {
// $routeParams will be populated here if
// this controller is used outside ng-view
});
);
As an alternate to the $timeout that plong0 mentioned...
You can also inject the $route service which will show your params immediately.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $route) {
console.log('routeParams:'+JSON.stringify($route.current.params));
});
I have the same problem.
What I discovered is that, $routeParams take some time to load in the Main Controller, it probably initiate the Main Controller first and then set $routeParams at the Child Controller. I did a workaround for it creating a method in the Main Controller $scope and pass $routeParams through it in the Child Controllers:
angular.module('MyModule')
.controller('MainController', ["$scope", function ($scope) {
$scope.parentMethod = function($routeParams) {
//do stuff
}
}]);
angular.module('MyModule')
.controller('MyCtrl1', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
angular.module('MyModule')
.controller('MyCtrl2', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
had the same problem, and building off what Andre mentioned in his answer about $routeParams taking a moment to load in the main controller, I just put it in a timeout inside my MainCtrl.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $routeParams, $timeout) {
$timeout(function(){
// do stuff with $routeParams
console.log('routeParams:'+JSON.stringify($routeParams));
}, 20);
});
20ms delay to use $routeParams is not even noticeable, and less than that seems to have inconsistent results.
More specifically about my problem, I was confused because I had the exact same setup working with a different project structure (yo cg-angular) and when I rebuilt my project (yo angular-fullstack) I started experiencing the problem.
You have at least two problems here:
with $routeParams you get the route parameters, which you didn't define
the file where you define a main controller doesn't really matter. the important thing is in which module/function
The parameters have to be defined with the $routeProvider with the syntax :paramName:
$routeProvider.when("/view2/name1/:a/name2/:b"
and then you can retrieve them with $routeParams.paramName.
You can also use the query parameters, like index.html?k1=v1&k2=v2.
app.js is the file where you'd normally define dependencies and configuration (that's why you'd have there the app module .config block) and it contains the application module:
var myapp = angular.module(...);
This module can have other modules as dependencies, like directives or services, or a module per feature.
A simple approach is to have a module to encapsulate controllers. An approach closer to your original code is putting at least one controller in the main module:
myapp.controller('MainCtrl', function ($scope) {...}
Maybe you defined the controller as a global function? function MainCtrl() {...}? This pollutes the global namespace. avoid it.
Defining your controller in the main module will not make it "to take effect in all routes". This has to be defined with $routeProvider or make the controller of each route "inherit" from the main controller. This way, the controller of each route is instantiated after the route has changed, whereas the main controller is instantiated only once, when the line ng-controller="MainCtrl" is reached (which happens only once, during application startup)
You can simply pass values of $routeParams defined into your controller into the $rootScope
.controller('MainCtrl', function ($scope, $routeParams, MainFactory, $rootScope) {
$scope.contents = MainFactory.getThing($routeParams.id);
$rootScope.total = MainFactory.getMax(); // Send total to the rootScope
}
and inject $rootScope in your IndexCtrl (related to the index.html)
.controller('IndexCtrl', function($scope, $rootScope){
// Some code
});

Resources