I'm a bit confused with the use of $scope in controllers and of scope in directives. Please verify if my understanding is correct (and also provide some alternative ways how to do this).
Let's say I have an html:
<div ng-controller="app1_Ctrl">
.
.
.
<input type="text" ng-model="value"/>
<input type="checkbox" />
<button ng-click="submit()"></button>
</div>
And my main.js
(function() {
angular.module('mainApp', ['app1']);
})();
And my app1 looks like this (based on official AngularJS documentation here)
(function() {
var app = angular.module('app1', []);
app.controller('app1_Ctrl', ["$scope", function($scope) {
.
.
.
}]);
app.directive('app1_Dir1', [function() {
function link(scope, element, attr) {
scope.$watch(attr.someAttrOfCheckBox, function() {
// some logic here
});
function submit() {
// some logic here
}
}
return link;
}]);
})();
How does $scope.value passed in scope in directive so that I can do some manipulations there? Will ng-click fire the function submit() in the directive link? Is it correct to use scope.$watch to listen for an action (ticked or unticked of course) in checkbox element?
Many thanks to those who can explain.
By default, directive scope is controller $scope; but it means the directive is directly dependent on your controller and you need a different controller for each instance of the directive you want to use. It is usually considered a best practice to isolate your directive scope and specifically define the variables you wish to pass it from your controller.
For this, you will need to add a scope statement to your directive :
scope {
label :'#',
context : '=',
function : '&'
}
and update your view :
<my-directive label="labelFromController" context="ctxtFromController" function="myFunction()" ></my-directive>
The symbols denote the kind of thing you wish to pass through : # is for one-way binding (as a string in your directive), = is for two-way binding of an object (which enables the directive to update something in your controller), and & is for passing a function.
There are a lot of additional options and subtleties that are best explained by the Angular doc https://docs.angularjs.org/guide/directive. There are also some nice tutorials out there (e.g. http://www.sitepoint.com/practical-guide-angularjs-directives/)
Your submit() function is not attached to anything, so you won't be able to call if from your viewer. You need to define it as scope.submit = function() ... in your link function if you wish to access it.
You can use $watch for this kind of thing, but there are usually other more elegant ways to achieve this by leveraging the fact that angular already "watches" the variables it is aware of and monitors any changes he can (this can be an issue when some external service changes data for exemple, because angular cannot listen to events it is not made aware of). Here, you can probably simply associate the ng-model directive to your input checkbox to store its true/fale (checked/unchecked) value, and the ng-change or ng-click directives to act on it. The optimal solution will mostly depend on the exact nature of your business logic.
Some additional thoughts :
The HTML insides of your directive should be packaged in an inline template field, or in a separate HTML file referenced by the templateUrl field in your directive.
In your HTML code above, your directive is not referenced anywhere. It should be an element, attribute or class (and your directive definition should reflect the way it can be called, with the restrict field). Maybe you have omitted the line containing the directive HTML, but as it stands, your directive doesn't do anything.
To my knowledge, you don't need to return link. Think of it as the "body" of your directive, where you define the variables and functions you will call in the HTML.
Your directive doesn't actually need HTML code and the above thoughts might be irrelevant if you are going in a different direction, but encapsulating some kind of view behaviour that you want to reuse is probably the most common use of directives.
Related
I know if I have two directives that are nesting I can communicate throw controller, require and pass it as the fourth parameter of the link function.
<my-first-div>
<my-seconded-div></my-seconded-div>
</my-first-div>
and every thing will work fine.
but I could do the same thing when they weren't nesting.
<my-first-div></my-first-div>
<my-seconded-div></my-seconded-div>
why ?
and how do I make them communicate ?
It happens since both of the directives have watchers on the same variable reference. So the changed value is being 'noticed' in all the relevant directives.
You could mimic this "communication" by passing the same variable (By Reference) (varName:'=') for both directives and place watchers on that variable inside each of these directives.
Then, the DOM hierarchy won't matter
For example:
Directive 1:
app.directive('directive1', function () {
return {
restrict: 'E',
scope: {
myVar: '='
}
link: function (scope, element, attrs) {
// do something with main directive
console.log("directive1", scope.myVar);
$scope.$watch("scope.myVar", function (value) {
console.log("directive1", "value changed to:" + scope.myVar)
});
}
}
});
The same for the second directive..
For both directives pass the same variable
And the magic will happen
I assume by saying communicating, you mean sharing data, state and events between two directives. I will list basic ways that I have in mind here:
The reason why you can pass data/state between two nested directives is because in AngularJS a child directive (nested one in your example) inherits the scope of it parents. As the results, two sibling directives can share same data from its parent controller.
<div ng-controller="ParentCtrl">
<ng-sibling></ng-sibling>
<ng-another-sibling></ng-another-sibling>
</div>
In the above piece of code, ng-sibling and ng-another-sibling will inherit the same data that is defined in their parent ParentCtrl
AngularJS support emitting/broadcasting event/data using $broadcast, $emit and $on function, document can be found here: https://docs.angularjs.org/api/ng/type/$rootScope.Scope.
$emit can be used to sent event upward the tree's hierarchy, while $broadcast's downward, and the direction is essential.
So one of your directive can dispatch the event, while the other listen to it. It's pretty similar to the way jQuery trigger events.
// In link function or controller of one directive
$scope.$broadcast("EVENT_A",my_data);
// Listen to EVENT_A on another directive
$scope.$on("EVENT_A",function($event,data){
....
})
While two-way binding or firing event arbitrarily can be useful at first, they can also lead to the situation when it's really difficult to keep track of the application's event and data flow. If you find this situation, maybe consider using Flux architecture with AngularJS not a bad idea. Redux fully supports AngularJS and all of your directives can be built as single-state components. The Github repo can be found here: https://github.com/angular-redux/ng-redux, and a simple tutorial on how to run AngularJS with Redux can be found here: http://blog.grossman.io/angular-1-using-redux-architecture/
I have a custom directive that I'm using in my templates. It does a bit of DOM work for me. I would like the host view/controller that I'm using the directive in to be able to run methods on my directive (and it's controller). But I'm not sure how best to call into the directives scope.
Example Fiddle
My view code:
<div ng-app="app">
<div ng-controller="MainCtrl">
<h3>Test App</h3>
<button ng-click="scopeClear()">Parent Clear</button>
<div my-directive string="myString"></div>
</div>
</div>
Here is the custom directive:
angular.module('components', []).directive('myDirective', function() {
function link(scope, element, attrs) {
scope.string = "";
scope.$watch(attrs.string, function(value) {
scope.string = value;
});
}
return {
controller: function($scope, $element) {
$scope.reset = function() {
$scope.string = "Hello";
}
$scope.clear = function() {
$scope.string = "";
}
},
template:
"<button ng-click='reset()'>Directive Reset</button>" +
"<button ng-click='clear()'>Directive Clear</button><br/>" +
"<input type='text' ng-model='string'>",
link: link
}
});
And controller:
angular.module('app', ['components']).controller('MainCtrl', function($scope) {
$scope.myString = "Hello";
$scope.scopeClear = function() {
// How do I get this to call the clear() method on myDirective
}
});
The workaround I found is jQuery('#my_directive').scope().myMethod(); But this seems wrong, like I'm missing some better part of angular to do this.
It also seems like and $emit isn't right here since I want a targeted method so it won't trigger on additional instances of the directive I have on the same page.
How would I access the directives methods from my parent controller?
I'm not sure I fully understand your objective here, and it's possible you could find a better pattern completely. Typically, directives display the state of the scope which is either an isolate scope (if they are self-sufficient) or a shared scope. Since you are not creating an isolate scope then they inherit the scope from the controller. If they are displaying data inherited from the controller then you don't want your controller calling into the directive, rather the directive will simply "redraw" itself whenever the properties in the controller change.
If you, instead, are looking to recalculate some stuff in your directives based on events from outside the directive you don't want any tight coupling - especially if building an entirely separate module. In that case, you might simply want to use $broadcast from the $scope within MainCtrl to broadcast an event that you may care about, and then your directive can provide the $on('eventName') handler. This way it's portable to any controller/scope that will fire such an event.
If you find yourself needing to know the exact properties in the controller or the exact functions within the directive then I would suggest that you have too-tightly coupled these pieces and they don't belong in separate modules since they could never be reused. Angular directives and controllers are not objects with functions, but objects that create scope and update frequently via $digest calls whenever properties in that scope change. So you may be able to find a way to better model the data, objects, and properties you are displaying. But I can't say without greater context.
I need to perform some operations on scope and the template. It seems that I can do that in either the link function or the controller function (since both have access to the scope).
When is it the case when I have to use link function and not the controller?
angular.module('myApp').directive('abc', function($timeout) {
return {
restrict: 'EA',
replace: true,
transclude: true,
scope: true,
link: function(scope, elem, attr) { /* link function */ },
controller: function($scope, $element) { /* controller function */ }
};
}
Also, I understand that link is the non-angular world. So, I can use $watch, $digest and $apply.
What is the significance of the link function, when we already had controller?
After my initial struggle with the link and controller functions and reading quite a lot about them, I think now I have the answer.
First lets understand,
How do angular directives work in a nutshell:
We begin with a template (as a string or loaded to a string)
var templateString = '<div my-directive>{{5 + 10}}</div>';
Now, this templateString is wrapped as an angular element
var el = angular.element(templateString);
With el, now we compile it with $compile to get back the link function.
var l = $compile(el)
Here is what happens,
$compile walks through the whole template and collects all the directives that it recognizes.
All the directives that are discovered are compiled recursively and their link functions are collected.
Then, all the link functions are wrapped in a new link function and returned as l.
Finally, we provide scope function to this l (link) function which further executes the wrapped link functions with this scope and their corresponding elements.
l(scope)
This adds the template as a new node to the DOM and invokes controller which adds its watches to the scope which is shared with the template in DOM.
Comparing compile vs link vs controller :
Every directive is compiled only once and link function is retained for re-use. Therefore, if there's something applicable to all instances of a directive should be performed inside directive's compile function.
Now, after compilation we have link function which is executed while attaching the template to the DOM. So, therefore we perform everything that is specific to every instance of the directive. For eg: attaching events, mutating the template based on scope, etc.
Finally, the controller is meant to be available to be live and reactive while the directive works on the DOM (after getting attached). Therefore:
(1) After setting up the view[V] (i.e. template) with link. $scope is our [M] and $controller is our [C] in M V C
(2) Take advantage the 2-way binding with $scope by setting up watches.
(3) $scope watches are expected to be added in the controller since this is what is watching the template during run-time.
(4) Finally, controller is also used to be able to communicate among related directives. (Like myTabs example in https://docs.angularjs.org/guide/directive)
(5) It's true that we could've done all this in the link function as well but its about separation of concerns.
Therefore, finally we have the following which fits all the pieces perfectly :
Why controllers are needed
The difference between link and controller comes into play when you want to nest directives in your DOM and expose API functions from the parent directive to the nested ones.
From the docs:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
Say you want to have two directives my-form and my-text-input and you want my-text-input directive to appear only inside my-form and nowhere else.
In that case, you will say while defining the directive my-text-input that it requires a controller from the parent DOM element using the require argument, like this: require: '^myForm'. Now the controller from the parent element will be injected into the link function as the fourth argument, following $scope, element, attributes. You can call functions on that controller and communicate with the parent directive.
Moreover, if such a controller is not found, an error will be raised.
Why use link at all
There is no real need to use the link function if one is defining the controller since the $scope is available on the controller. Moreover, while defining both link and controller, one does need to be careful about the order of invocation of the two (controller is executed before).
However, in keeping with the Angular way, most DOM manipulation and 2-way binding using $watchers is usually done in the link function while the API for children and $scope manipulation is done in the controller. This is not a hard and fast rule, but doing so will make the code more modular and help in separation of concerns (controller will maintain the directive state and link function will maintain the DOM + outside bindings).
The controller function/object represents an abstraction model-view-controller (MVC). While there is nothing new to write about MVC, it is still the most significant advanatage of angular: split the concerns into smaller pieces. And that's it, nothing more, so if you need to react on Model changes coming from View the Controller is the right person to do that job.
The story about link function is different, it is coming from different perspective then MVC. And is really essential, once we want to cross the boundaries of a controller/model/view (template).
Let' start with the parameters which are passed into the link function:
function link(scope, element, attrs) {
scope is an Angular scope object.
element is the jqLite-wrapped element that this directive matches.
attrs is an object with the normalized attribute names and their corresponding values.
To put the link into the context, we should mention that all directives are going through this initialization process steps: Compile, Link. An Extract from Brad Green and Shyam Seshadri book Angular JS:
Compile phase (a sister of link, let's mention it here to get a clear picture):
In this phase, Angular walks the DOM to identify all the registered
directives in the template. For each directive, it then transforms the
DOM based on the directive’s rules (template, replace, transclude, and
so on), and calls the compile function if it exists. The result is a
compiled template function,
Link phase:
To make the view dynamic, Angular then runs a link function for each
directive. The link functions typically creates listeners on the DOM
or the model. These listeners keep the view and the model in sync at
all times.
A nice example how to use the link could be found here: Creating Custom Directives. See the example: Creating a Directive that Manipulates the DOM, which inserts a "date-time" into page, refreshed every second.
Just a very short snippet from that rich source above, showing the real manipulation with DOM. There is hooked function to $timeout service, and also it is cleared in its destructor call to avoid memory leaks
.directive('myCurrentTime', function($timeout, dateFilter) {
function link(scope, element, attrs) {
...
// the not MVC job must be done
function updateTime() {
element.text(dateFilter(new Date(), format)); // here we are manipulating the DOM
}
function scheduleUpdate() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateTime(); // update DOM
scheduleUpdate(); // schedule the next update
}, 1000);
}
element.on('$destroy', function() {
$timeout.cancel(timeoutId);
});
...
How to set ng-controller as an expression from the $scope?
According to the documentation:
ngController – {expression} – Name of a globally accessible
constructor function or an expression that on the current scope
evaluates to a constructor function.
But how to evaluate scope expression as a controller for controllers that have been registered with module .controller?
For example:
Layout:
<div ng-controller="myExpr"></div>
JavaScript (define controller):
app.controller('myCtrl', ['$scope', '$timeout', function () { ... }];
JavaScript (parent scope):
$scope.myExpr = ...;
What should be in myExpr to use myCtrl as a controller via expression?
I've tried $controller('myCtrl')... not working...
P.S. If controller has been defined via globally accessible function.. it's possible to provide it as myExpr. But what to do if it has been defined so?
The expressions that ng-controller accept are a bit wierd. So you can do this by writing your controller slightly differently (but read below as for why you probably don't want to).
function myCtrl($scope) {
$scope.value = 'Stuff';
}
This is a controller and will work like normal for this case. Like in this example: http://jsbin.com/ubevel/2/edit
So why not do it?
First of all this is not a good way to define things from a testing perspective. Secondly, this allows you to set the controller dynamically on load but it won't allow you to change things after that. If you change the value of myExpr after the page has loaded the controller will not change.
So what to do?
I would highly suggest looking at using a service instead. Swap out your actions by supplying your outer controller with a service that you then change in the same manner you are now trying to change the inner controller. So something like: http://jsbin.com/ubevel/5/edit
This service can be swapped out on the fly, changing it will change the actions that are available in the scope.
You could also use an include, but this would result in duplicate html. I personalty am fine with this since I am against reusing html for two different types objects (sooner or later you want to change one but not the other and it becomes a mess). But a lot of people would object to that.
An extra note: There are probably nicer ways to do it with controllers, I probably haven't looked at all angles, but I just don't think controllers are the right tool for this case.
Within a directive definition, there's an API for accessing $element and $attrs, so you can refer back to the element from which the directive was called. How would I access $element and $attrs when calling a custom function using a standard directive like ng-class?
EDIT: I understand that this is not an idiomatic approach. This question is applicable to rapid prototypes, which is a great use for many of Angular's features. Just not the ones that are all about sustainability, separation of concerns, etc. My primary concern is velocity. Thus, being able to bang something out quickly with the built-in directives and a quick controller method can, in fact, be a virtue, and can win the opportunity to do a fuller and more proper implementation down the road...
In this case, I'm just adding a contextual .active class to a nav element, based on the value of $location.path(), as per this post and this one. However, in those examples you need to explicitly and redundantly pass a copy of the contents of the href attribute as an argument to the getClass() function you're calling from ng-class. But isn't there a way to programmatically access the href attribute from within getClass() and avoid redundantly passing identical content as an arg?
For example, a controller with a getClass() function the way I'd imagine it could work:
function navCtrl($scope, $routeParams) {
$scope.getClass = function($element, $attrs) {
if ($location.path().substr(0, path.length) == $attrs.href) {
return "active"
} else {
return ""
}
}
}
Which you could then call simply and elegantly with:
<a ng-class="getClass()" href="/tasks">Tasks</a>
rather than:
<a ng-class="getClass('/tasks')" href="/tasks">Tasks</a>
(I recognize another option is to create a custom directive that does this, but for now I'd just like to figure out if it's possible to access $attrs and/or $element from within a controller function that's been called by a directive. Thanks!)
You actually can do this... BUT YOU SHOULD NOT DO THIS. lol...
Here's how to do what you're asking...
In your markup, pass $event into your function.
<a ng-click="test($event)" href="whatever">Click Me</a>
Then in your controller, get the target element of the event:
$scope.test = function($event) {
alert($event.target.href);
};
Now here is why you probably shouldn't do this:
If you reference the DOM, or manipulate the DOM in your controller, you're endangering dependency injection and also the separation of concerns from within the Angular structure. Sure you could still inject $event as a dependency if you were testing your function, but depending on what you're doing inside of that function, you might still have ruined your DI, and you're trying to make a controller do a directive's work (which is to try to keep your controller from being tightly-coupled to your markup)
That said, if ALL your doing is just getting a value, it's probably fine, but you're still coupling your controller to your markup to some degree. If you're doing anything else with that DOM element, you're off the reservation.
I don't think you can do that. The angular philosophy is to avoid accessing the DOM directly from your controller. You have already identified your two options for doing this in Angular:
pass in the href as a param i.e.. getClass('/tasks')
or, write a custom directive
Alternatively, if the class is purely presentational and doesn't affect how your application runs then you could ignore angular and use a quick and dirty jQuery function to do the job for you.
The lack of direct interaction with the DOM can be a bit strange at first but it's a godsend in the long term as your codebase grows, gets more complicated and needs more tests.
Here's a completely different answer: you don't need a Angular to change styling based on href. You can use CSS selectors for that.
In your styles:
a[href="/something"] {
background-color: red;
}
That in combination with anything you might be doing with ng-class should be everything you need.