The initial setup:
<div ng-app="app">
<dumb-password></dumb-password>
</div>
var app = angular.module("app", [])
app.directive("dumbPassword", function () {
return {
restrict: "E",
replace: true,
templateUrl: "dumbpass.html",
link: function (scope, element) {
scope.$watch("model.input", function (value) {
if(value === "password") {
element.children(1).toggleClass("alert-box alert");
}
});
}
};
});
<div>
<input type="text" ng-model="model.input">
<div>{{ model.input }}</div>
</div>
Note: this was slightly modified from the video code for better readability
The directive injects the dumbpass.html template to replace the dumb-password element, which is a simple text field bound to model.input, which is echoed in the following div.
scope.$watch
Invoking scope.$watch on model.input sets a listener on that model and waits for it to change. When it changes, the value parameter can be used to check the new value in the listener function.
This blog post goes into more detail about how watching/listening works.
In our example, the listener checks if model.input is the string "password". If it is, it toggles css classes on element.children(1) using the AngularJS jQuery lite method toggleClass(). element is the top-level entity in the directive, and since here we're replacing the <dumb-password> entity, the top level entity is the first <div> in dumbpass.html, and we care about the 2nd entity in it, thus element.children(1). The traditional methods of locating entities with jQuery selectors are not supported in AngularJS jqLite.</div></dumb-password>
Refactor
Since element.children(1) is a brutish way of referencing that entity, we can instead get a reference to the element before it is put on the DOM, via angular.element.
We remove the entity from the template,
<div>
<input type="text" ng-model="model.input">
</div>
angular.element acts as the interface between jQuery/jqLite and AngularJS. The docs on the angular.element API are excellent for furthering understanding. Something to note is that all element references in Angular are always wrapped with jQuery or jqLite; they are never raw DOM references
Use angular.element to create an element from the string:
app.directive("dumbPassword", function () {
var validElement = angular.element('<div>{{ model.input }}</div>');
return {
restrict: "E",
$compile
The compile service takes an HTML string and compiles it into a template function, returning a link function which is used to bind template to a scope. Calling this linking function will then return the element of the template.
With this, the compile function is introduced to append the element:
return {
restrict: "E",
replace: true,
templateUrl: "dumbpass.html",
compile: function (tElem) {
tElem.append(validElement);
return function (scope) {
scope.$watch("model.input", function (value) {
if(value === "password") {
validElement.toggleClass("alert-box alert");
}
});
}
}
}
Above, the tElem parameter is the jqLite wrapper of the dumbpass.html template. Since we created the angular.element reference before as a variable in the directive, the tElem jqLite API is used to append the element validElement.
The $compile service returns a link function, which is identical to the previous one except for the fact that we can now reference validElement and use the jqLite API method on it instead of invoking element.children(1).
One last refactor that can be made is to move the link function into a directive variable, which is a bit cleaner. The final main.js is as follows:
var app = angular.module("app", [])
app.directive("dumbPassword", function () {
var validElement = angular.element('<div>{{ model.input }}</div>');
var link = function (scope) {
scope.$watch("model.input", function (value) {
if(value === "password") {
validElement.toggleClass("alert-box alert");
}
});
};
return {
restrict: "E",
replace: true,
templateUrl: "dumbpass.html",
compile: function (tElem) {
tElem.append(validElement);
return link;
}
}
});