What are the best practices of using ngInclude with ControllerAs syntax? - angularjs

I had the intention to use one template across several views having different controllers.
But now I realize that I cannot just write universal binding in templates because values will be put inside $scope.concreteControllerName.
Angular docs for ngInclude say that
This directive creates new scope.
I could use ng-init directive and pass controller instance to template's scope:
<ng-include src="..." ng-init="controller=concreteControllerName"/>
or even better
<ng-include src="..." ng-init="model=getModelForTemplate()"/>
and then write {{controller.boundvalue}} in template.
That is a working solution, I guess.
And here I'd like to know whether other better approaches exist and if not, should templates always be used with some notion of passed model to abstract away from parent scope?

Use John Papa's controllerAs View Syntax and controllerAs with vm. You specify different controllers in the ng-include directives but use the same src html template. The common vm variable name is used in the template.
index.html
<div ng-include ng-controller="controllerOne as vm" src="'same.html'"></div>
<div ng-include ng-controller="controllerTwo as vm" src="'same.html'"></div>
<div ng-include ng-controller="controllerThree as vm" src="'same.html'"></div>
controllerOne.js
function controllerOne() {
var vm = this;
vm.name = 'Controller One!';
sharedTemplate.html
<div>{{vm.name}}</div>
Here is a full working version: Full Working Code in Plunker

Related

How to use controller with custom directive in AngularJS?

Let's say I am using a custom directive named my-form-data:
<my-form-data info='infoObj1' template="ssc.html"/>
<my-form-data info='infoObj2' template="college.html"/>
Inside directive definition, I want a different templateUrl based on the template attribute of the directive on the HTML page.
Is there any way to specify the controller class associated with ssc.html and college.html?
Use different directives. Its just a couple lines of code and they'll have their own template HTML. They can all share the same controller if desired or have their own. It doesn't make sense to try and use the same directive if you have different views and different controllers.
To answer your question directly, you can specify the controller in your directive HTML using ng-controller.
ssc.html
<div ng-controller='sscCtrl'>
...
</div>
college.html
<div ng-controller='collegeCtrl'>
...
</div>

Why can't we use the "controller as" syntax in AngularJS when replacing "this" with $scope?

Consider this :
<!doctype html>
<html ng-app="some">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
</head>
<body>
<div ng-controller="helloCtrl as hello">
<label>Name:</label>
<h1>Hello {{hello.yourName}}!</h1>
</div>
</body>
<script>
angular.module("some", []).controller("helloCtrl", function($scope){
$scope.yourName = 'Alice'
})
</script>
</html>
If I change $scope.yourName = 'Alice' to this.yourName = 'Alice', it works. Why not before ?
Similarly, if I keep $scope.yourName = 'Alice'and still use the as controller syntax, it doesn't work.
I am asking because I read somewhere that this is set to $scope by AngularJS internally.
First, an optional exercise to "peek under the hood". If you're using Chrome, install Batarang, inspect your controller div and have a look at the $scope tab. If you're not using controllerAs, you'll see all your controller properties, but if you're not, you'll only see a single property with a name corresponding to your controller alias. All your properties will then be below this in the tree.
A thing that is easy to forget with Angular is that controllers are just JS objects, instantiated by Angular when your page loads. In your first example, when your controller is instantiated it adds the yourName property to the $scope object that was passed to the constructor. To get the value of this property, use $scope.yourName - although within your handlebars, Angular will add the $scope. prefix for you.
I read somewhere that this is set to $scope by AngularJS internally
This is where the misunderstanding is. In the second example, yourName is a property of your controller, not a property of $scope. This is where controllerAs comes in - no magic, just a little syntactic sugar. Here, when the controller is instantiated and its properties set with this, Angular creates a property on $scope with the name corresponding to controllerAs and a value of your controller object. The value of this property is therefore at $scope.hello.yourName - hence writing hello.yourName in your handlebars.
Sources:
Angular Controller Documentation - Lots of obvious "how", but there's a little (rather dry!) info on how things work underneath
The Angular Style Guide on Controllers - Required reading for Angular devs IMO. Explains a lot of the "why" clearly and concisely, as well as the "how".
Actually, $scope is tied to scope, so if you have something like this in your controller:
$scope.Alice = "Alice";
you use this in HTML:
{{ Alice }}
But if you use this, it's not tied to the scope anymore and your controller has
this.Alice
then you write it as {{ controllerAlias.Alice }} to tell Angular it's part of controller, not of scope.(i.e. just Alice won't work anymore).
About the second part of your question, you could use hello.Alice without using alias by doing this:
$scope.hello = this;
This is implicitly done when using controller as(it wasn't there in older versions of Angular).

AngularJS Child Scope for Element

I have a reusable template called profile.html. It looks something like this:
<div>
{{firstName}}
</div>
I have it embedded in another template which is bound to a dedicated controller:
<div ng-include src="'templates/profile.html'"></div>
I want a child $scope created for this div. In the controller for the parent template, I have something like:
$scope.profile = theProfile;
I want the child scope for the profile.html template to be the parent $scope.profile. Something akin to:
<div ng-include src="'templates/profile.html'" ng-scope="{{profile}}"></div>
How can I do this?
It looks like you're basically reinventing directives, by trying to set both the template and scope like that. Also, $scope is an object with a large amount of other properties/objects on it, so setting it to a be another object would be... problematic.
The following would create a directive that merges a passed in profile to the $scope using angular copy, if you really want to do it that way. I'd recommend just using a $scope.profile, though.
.directive('profile', [function(){
return{
templateUrl:'templates/profile.html',
scope:{profile:'='},
controller: function($scope){
angular.copy($scope.profile, $scope) // if you really, really want the properties right on the scope.
}
}
}]
ngInclude automatically creates a child scope. You shouldn't need to explicitly pass some data to it since it can access its parent scope via prototypical inheritance (this might become a problem if your template changes the scope).
The problem here is that your template expects a firstName property to exist in the scope, but it doesn't. So you could change your template to
<div>
{{profile.firstName}}
</div>
but that would couple the template to the profile object, which might be a bad idea.
Another solution would be to manually create the firstName property in the correct scope:
<div ng-include src="'templates/profile.html'"
ng-init="firstName=profile.firstName">
</div>
I'm not very fond of this solution, though, because it can easily get out of hand if the template needs more properties and it breaks the template encapsulation to some extent.
And finally, you could wrap that template within a directive:
directive('whateverMakesSense', function() {
return {
restrict: 'E',
template: '<div>{{data.firstName}}</div>',
scope: { data: '=' }
};
});
...
<whatever-makes-sense data="profile"></whatever-makes-sense>
If you find yourself using that template in many places, I suggest you go for the custom directive approach. It will give you more control, things will be better encapsulated and as a bonus your markup will be more semantic - if you use anything but whatever-makes-sense, of course. :)

Can angularJS manage multiple transclusions inclusion?

I am trying to make a transitive transclusion, or call it “directive inception”.
I made this example to illustrate what I am trying to do:
http://plnkr.co/edit/0hFFHknDps2krtK1D9ud?p=preview
The directive “first” wraps the directive “second” in its template and the two of them use transclusion.
What I want to do is to bind a value from a controller to the html that is a child of the “first” directive.
So I wanted my example to display:
<h1>Chained transclusions test</h1>
<div>
<h2>First directive</h2>
<div>
<h2>Second directive</h2>
<div>Controller hello</div>
</div>
</div>
Obviously that is not what I got.
I tried to analyze the scope with the developer tool and I was surprised by the result scope tree:
the result trees
I thought angularJS would create a new scope when using the transclude feature in a directive. And that this scope would be a non isolate sibling of my directive isolate scope. But I cannot see any sibling of my first directive scope (although it uses transclude). Plus, every children of my “first” directive has a scope isolated from the controller scope since the “first” directive scope is an isolated one.
I don’t understand the behavior here.
Is the transclude inclusion completely forbidden in angularJS ?
Is it possible to create a directive with transclusion, that wraps another directive that uses transclusion ?
It seems to me that this is the whole power behind web components, the fact that transclusion or any other special caracteristics should be seen as “implementation detail”, and the component should be able to use other directives that hide their own implementation details.
Without getting into the details of scope creation when using isolate scope with transclusion... it is possible to nest transclusions, but in your example, you need to make scope.controllerMsg available to the first directive's isolate scope:
JS:
app.directive('first', function(){
return {
...
scope: { controllerMsg: '=text'},
...
}
});
HTML:
<first text="controllerMsg">
{{controllerMsg}}
</first>
Demo

Angular.js scope issues during template loading

I've a two directives with transcluding html which calls isolated scope.
This Plnkr works fine while templates are inline, but if I change template to templateURL, it stops work.
Are there any issues with compiling?
Loading the template use URL, Angularjs creates an additional transcluded scope I guess.
Try to use $$prevSibling.$$prevSibling to access the functions.
Btw, it is really hacky to use $$prevSibling.
<div authorization>Sign in</div>
<div registration>Registration</div>

Resources