Accessing The View-Model Inside The Link Function When Using Controller-As In AngularJS
Historically, the Scope object has been the view-model in AngularJS. But, in recent times, people have begun shifting away from direct Scope access and moving towards the use of the Controller itself as the view-model. This can be accomplished with the "controller as" syntax in newer versions of AngularJS as well as with custom directives in older versions of AngularJS. When you use this approach, it may not be immediately obvious how you access the view model from within the link function of a component directive. As such, I wanted to whip together a quick demo.
Run this demo in my JavaScript Demos project on GitHub.
Before this shift towards the "controller as" approach, the Scope instance was the view-model. And, since the Scope is the first argument injected into every directive link() function, it made it extremely easy to consume the view-model from within the link() function. But, now that the Scope is being isolated for events and digest-access, where is the view-model?
It's still being injected, it's just a different argument. With the use of the "controller as" syntax, we are telling AngularJS that we are intending to use the Controller instance as the view-model. This holds true for both our template as well as our link() function. As such, the injected controller is now our view-model access.
When defining the directive configuration object, we can explicitly "require:" the controller (aka, our view-model); or, we can just let it be injected implicitly - anytime we define the "controller" property, the controller is automatically injected into the link() function. In the following demo, I'm using the link() function as the "glue" between user-initiated click-events and the view-model methods:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Accessing The View-Model Inside The Link Function When Using Controller-As In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Accessing The View-Model Inside The Link Function When Using Controller-As In AngularJS
</h1>
<div bn-widget starting-at="5">
<!-- Content to be supplied by component. -->
</div>
<!--
I am the widget component template (inlined).
--
NOTE: Since we are using the "controller as" syntax, the view model is
being exposed in the directive template as the "vm" object.
-->
<script type="text/ng-template" id="widget.htm">
<div class="m-widget">
Click count: {{ vm.clickCount }}
</div>
</script>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the view-model for the widget.
angular.module( "Demo" ).controller(
"WidgetController",
function WidgetController( $scope ) {
// Using this pattern allows us to maintain a reference to the THIS
// scope as a means to "reveal" public properties and methods for use
// as the "view model". It also has the added benefit of providing a
// lexical binding which can be referenced inside of closures.
var vm = this;
// Since we are using the Controller instance as the view model, we
// can use the incoming isolate $scope as the ReactJS-inspired "props"
// object. I feel like this creates a really nice separation of origins.
var props = $scope.props = $scope;
vm.clickCount = ( props.startingAt || 0 );
// Expose the public API.
vm.incrementClickCount = incrementClickCount;
// ---
// PUBLIC METHODS.
// ---
// I increment the count and return the new value.
function incrementClickCount() {
return( ++vm.clickCount );
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I define the widget directive, gluing the template to a controller.
angular.module( "Demo" ).directive(
"bnWidget",
function WidgetDirective() {
// Return the directive configuration object.
// --
// NOTE: By using the "controllerAs" property, we are telling AngularJS
// that we *intend* to use the Controller instance as the view-model in
// our template and our link function. As such, AngularJS will expose the
// controller on the scope using the given variable name.
// --
// CAUTION: Since we defining the "controller" property, the current
// controller will automatically be injected into the link() function
// as the fourth argument.
return({
controller: "WidgetController",
controllerAs: "vm",
link: link,
scope: {
startingAt: "=?"
},
templateUrl: "widget.htm"
});
// I bind the JavaScript events to the view-model (which, in this case,
// is the controller instance).
function link( scope, element, attributes, controller ) {
element.on( "click", handleClick );
// When the user clicks inside the current element, we need to
// consume the .incrementClickCount() on the view-model. Since we
// are using the "controller as" syntax, this means that our
// Controller IS OUR view model; the public properties and methods on
// the controller are the properties and methods of our view model.
// As such, in order to access the view model from within our link
// function, we just need access to the Controller, which has been
// injected into the link() function.
function handleClick( event ) {
scope.$apply(
function changeViewModel() {
controller.incrementClickCount();
}
);
}
}
}
);
</script>
</body>
</html>
As you can see, the component controller is exposing the ".incrementClickCount()" as part of the view-model. Our directive link() function can then consume this method directly on the injected controller instance.
Once you see how this works, it makes a lot of sense. But, if you are coming from a background in which the Scope object is the view-model, it can be a bit confusing when switching to the use of the controller as the view-model. Hopefully this post helps clarify how that transition can be pulled through to all aspects of your component directies in AngularJS.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben,
other way to access controller content in link function when we define controllerAs, is access this name in the scope object... in your example with controllerAs = vm, in link function we can access using:
scope.vm
ps.: I figure out this, when I needed write some test code to directives :D
@Erko,
Yes, great point. The only reason I wanted to shy away from that was to emphasize the Controller as the point of data access and to deemphasize the Scope as the point of data access.
That said, understanding the way "controller as" works is very important, both in the context of component directives as well as in the context of the ng-controller directive (were you can use the `MyController as vm` syntax). Ultimately, all this is doing, as you point out, is setting the controller IN the scope.
Hi Ben,
do you know what happens to the controller argument if we "require" controllers in the directive. With "require: ['ngModel']" it is an array containing the ngModelController only. I don't find a way to access the controller, especially when not using controllerAs, when I can access it from the scope.
@Hansmaad,
require: ['ngModel', 'myDirectiveName']
Controllers will be passed as an array. The directive string has to be the exact string that defines the directive.
@Hansmaad, indeed if you are ysing `require` on the DDO, the 4th argument of the linking function will depend on what you require. If you also need to access the directive's controller, you have to require it too.
E.g.:
.directive('bnWidget', function bnWidgetDirective() {
return {
...,
require: ['bnWidget', 'ngModel'],
link: function bnWidgetPostLink(scope, elem, attrs, ctrls) {
var bnWidgetCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
...
}
};
})
What about using the bindToController property in the directive configuration object?
Setting the bindToController property to true enables the isolate scope properties to be bound to the controller, not the $scope Object. Hence avoiding the need to inject the $scope object into the controller.
Here you can see the bindToController property at work https://github.com/dario-campagna/JavaScript-Demos/blob/master/demos/accessing-vm-in-link-angularjs/index.htm
Hey Ben,
Why did you chose to use 'element.on ("click" ... ' in the link section instead of using ng-click?
Is it only for the demonstration? or is it kind of a best practice?
Thanks,
Shai.
You said, 'we can just let it be injected implicitly - anytime we define the "controller" property, the controller is automatically injected into the link() function'. Note that injection is NOT used when calling the link function - arguments are passed as normal JS function arguments and the 4th parameter will be the single controller (or array of controllers). You can call it what you like ('controller', 'controllers', 'myController').
@Robin,
Ah, excellent clarification. Yeah, it was a poor choice of terminology on my part. By "injection," I only meant to imply that it was being supplied by AngularJS based on the configuration of the directive. But, this is definitely NOT the same thing as dependency injection used by the Injector instance.
As an aside, I have also changed my variable names based on this very divergence:
www.bennadel.com/blog/2716-when-to-use-scope-vs-scope-in-angularjs.htm
When I talk about "link" functions, I no longer use "$scope", only "scope". Because, as you articulate, link functions don't use "injection".
Thanks for the clarifying my choice of words.
@Shai,
Great question - I only used the .on(click) as opposed to ng-click so that I could have a reason for the link() function to call the controller. Otherwise, I wouldn't have needed the link() function at all for this demo. That said, I would generally use ng-click and then only defer to the link() function for special interaction use-cases.
@Dario,
To be honest, I'm on the fence about the bindToController property. And, after having played around with ReactJS a bit, I actually prefer NOT to use the property at all. The reason for this is that it creates a meaningful separation between the "incoming" scope values and the "internal" view-model values.
So, what I end up doing, actually, is creating two aliases relating to the scope:
var vm = this; // The controller-as view-model.
var props = $scope.props = $scope; // The incoming props.
This way, it makes the code very obvious as to whether or not the value is something that I should consider "read only"; or, if it is something I can change. If I see this in the code:
var x = props.someValue;
... I know two things: 1) "someValue" is being provided via the isolate scope (via scope:{someValue:"="}. 2) It should be a "read only" variable - we never want to "set" a value that is passed into the isolate scope.
For a better explanation, check out this post:
www.bennadel.com/blog/2895-creating-a-reactjs-inspired-props-object-in-angularjs.htm
This article is exactly what I needed, thanks!
I couldn't get my directive "Employee Search" that uses a jQuery UI Dialog to work properly. The solution was that instead of putting everything in the directive's controller function.... I broke it in two pieces.... (a) most everything still left in the directive's controller.... and (b) after reading your article, I added a link function where I moved the dialog instantiation code.
I had to do this part in the link function so that the scope had already been associated with my directive's template.... BEFORE invoking $('myDiv').dialog(...); which will actually remove 'myDiv' element from it's current spot in the DOM And throw it somewhere else (part of jQuery UI Dialog's behavior).
Inside the link function I was able to assign $scope.vm.theDialog = ...; Now my functions back in the directive controller can reference vm.theDialog.
So yeah, thanks!
@Erko, @Ben,
Thanks for this tip. This was the only way I could access directive's own controller, as I was importing controller from parent directive.
Like this:
app.directive('myDir', function() {
function mylink(scope, el, attrs, controller){
scope.someFn() = function() {
// here is calling some fn from parentDirective controller
controller.parentCtrlFn();
// here is calling some fn from own controller
scope.mcvm.ownCtrlFn();
};
}
function myctrl(){
var vm = this;
vm.ownCtrlFn = function(){
console.log('functioning on my own');
};
}
return {
restrict: "E",
require: '^parentDirective',
templateUrl: 'mytemplate.html',
link: mylink,
controller: myctrl,
controllerAs: 'mcvm',
bindToController: true
};
});
I hope this helps clarify the subject even more.
@Jason,
I'm glad this was helpful. Dealing with jQuery and jQuery UI components is always a little bit tricky because just about all access to a jQuery UI component happens at the DOM level. Even calling methods on the component happens at the DOM wrapper level. But, it sounds like you've found a nice balance. Good stuff!
@Miloš,
While you're on the topic of actually injecting / requiring multiple controllers into the link function, you should check out this recent post:
www.bennadel.com/blog/2969-passing-ngmodelcontroller-into-a-component-directive-controller-in-angularjs.htm
In that post, I'm using "require" to include both the current component controller as well as the ngModelController. I'm then using the link() function to inject the ngModelController into the component controller. Might be interesting to you.
@Ben,
Wow, thanks, I didn't even know ngModelController existed. Very useful! Also didn't know you could access controllers as an array.
@Miloš,
Glad to help :D Also, accessing the controller as an Array only happens if you *require* more than one controller. When you require a single controller, it comes through as a single reference. When you require multiple controllers, they come through as an array in the same order that they were required.
hello, good demo. i checked also the john papa style, but i have a question: how the interact or to use "Controller As and the vm Variable" with formular (i.e: complex sign up/admission form for university)? if i ask it's because i'm a beginner and i don't handle very well.
Ben ... this post was a life saver, thank you. I couldn't figure out how to do this, and I was getting so frustrated that all the examples on the AngularJs.org site are still using the "scope" as the VM.
Excellent post Ben. Glad to have stumbled across your site today as a few of your posts have been equally as helpful and crystallizing as this one.
What about changing property value from controller inside directive. It seems that it's loosing 2 way data-binding. Ex.
controller.someProp = "Somevalue";// doesn't work
It's not being reflected in controller, but if it's gone through scope it's fine:
scope.controllerAs.someProp = "Somevalue"' //works
@Rajeev, @Chris,
Glad you fellas found this post helpful, thanks!
@Beta,
Hmm, I can't think of a reason why mutating the property directly on the controller would fail. That said, if there is a property called "controllerAs" on the scope, I suspect that something is being misconfigured somewhere. When you use the "controllerAs", all that it's really doing is saving a controller reference under that property name; so, there's shouldn't be an actual scope-based key called "controllerAs". Unless you were just doing that to illustrate the object path?
Is is possible you aren't triggering a digest properly? If you are in the link() function and you change the view-model, you'll need to tell Angular that this change too place, with something like an $apply() or an $evalAsync().
@Ben,
While i was preparing a plunker for this case, I did some changes on the naming of controllers and I saw it was working. I investigated a bit this naming issue to my project where the problem was happening and it was strange that a certain name of controller in directive was causing this and still does. So mutating prop from controller inside directive it works fine. Regarding "controllerAs" i was meaning the name of controller past in directive that will be in the scope controllerAs:"someCtrl" then in scope scope.someCtrl, so i was referring with controllerAs to any alias given to directive's controller. Anyway I think this forced me to make a plunker and pushed me to solution.
Thank you