Combining AngularJS with existing Components(angularjs 中使用已有的控件)
This is a preview-quality chapter of our continuously deployed eBook on AngularJS for .NET developers. You can read more about the project at http://henriquat.re. You can also follow us on twitter (Project:@henriquatreJS, Authors: @ingorammer and @christianweyer)
Combining AngularJS with existing Components
A common question when looking at AngularJS for the first time is about how to combine it with existing JavaScript code and components. For jQuery UI in particular, the team around AngularJS has created the AngularUI project (http://angular-ui.github.com/) which provides an easy way to use jQuery UI with Angular.). For other frameworks - or especially for your custom DOM-manipulating code - the necessary steps might not be as obvious at the first glance.
As you have seen in the previous chapters, AngularJS puts a very strong focus on separation of concerns:
- Controllers expose model objects and methods to a view (via the use of $scope). They contain view-specific code without any DOM manipulations.
- Services contain methods which perform view-independent operation. Business-logic code which is used by multiple views and/or in different contexts is right at home here. (Note: some global services might even operate on the DOM level -- for example to show modal dialog boxes or similar. These manipulations should however be independent from of concrete view in your application.)
- Directives are re-usable components which bind model values to DOM properties and react on DOM events to update model values
Most existing components (especially in the jQuery-world) usually work in a different way. Without Angular, when you wanted to achieve a certain behavior, like displaying a date-picker, you would usually go with a jQuery UI extension (like 'datepicker') and use call similar to $("#someinput").datepicker({...})
. This would extend a standard <input>
element with an ID of someinput
to be turned into a datepicker.
In this command's options, you would have usually specified a callback to be invoked whenever the user selects/changes the date in the input box. But you wouldn't do this just for a single datepicker -- no, this kind of jQuery UI-extension would be littered throughout your business code for nearly every input control in your HTML. After all, your datepickers need access to the underlying model values to restrict the input to valid ranges and to perform additional validation if necessary. This mix of business code and DOM-manipulating code is sometimes the reason for maintenance headaches of JavaScript developers.
With this in mind, you can see the conundrum: how do you take classic JavaScript code (with this mix of DOM interactions and event handlers with direct manipulation of underlying business objects) and put it into the well defined structure of AngularJS?
So let's just see how we're going to take a jQuery-based fragment and move it forward to a reusable AngularJS directive. In the following example, you'll see a simple text entry box which is treated as a jQuery UI datepicker. When changing the date, the underlying business object will be updated and the value will be shown underneath the input box.
This demonstration consists mainly of two parts. The first is the rather straightforward and easy to understand HTML markup:
<div>
Date Of Birth:
<input type="text" id="dateOfBirth">
<br>
Current user's date of birth:
<span id="dateOfBirthDisplay"></span>
</div>
So far, so good. Now let's have a look at the jQuery-based code behind this HTML:
$(function () {
var user = {
dateOfBirth: new Date(1970, 0, 1)
};
var displayValue = function () {
$("#dateOfBirthDisplay").text(new Date(user.dateOfBirth).toDateString());
};
var processChange = function() {
user.dateOfBirth = $("#dateOfBirth").datepicker("getDate");
displayValue();
};
$("#dateOfBirth").datepicker({
inline: true,
onClose: processChange,
onSelect: processChange
}
);
displayValue();
// initial display of value in input-box
$("#dateOfBirth").datepicker("setDate", user.dateOfBirth);
});
Please note that this fragment, even though it already mixes code from different areas of responsibility (DOM manipulation, data conversion, business object population) is hardly complete: in practice, you'd quite likely also have to add validation code to check for date formats and ranges. But let's keep this simple for now.
Now, if code like this would only occur once throughout your application, its use could be quite acceptable. Unfortunately, without a framework like AngularJS, code like this would be written in multiple throughout your application. In fact, whenever you'd have a date-entry box, you would quite likely see code like this. As you can imagine: maintenance of a codebase like this might be quite a bit harder than it should be.
As you've seen before, AngularJS allows you to separate the concerns if your code into three different parts: the model, the controller and directives which perform DOM manipulation. So let's look at this particular example and how we can transform this into a more suitable - and maintainable - form.
Separating the Concerns
At first, we will change the HTML markup to tell AngularJS which internal application name should be used (as we will register the directives with this application). In addition, we'll already add the necessary data-binding information and add the directive my-datepicker
instead of the <input>
-element which we've used before. (Please note: this particular directive does not yet exist in AngularJS, but we'll build it throughout the remainder of this chapter.)
<div ng-app="demo" ng-controller="DemoController">
Date Of Birth:
<my-datepicker type="text" ng-model="user.dateOfBirth" />
<br>
Current user's date of birth:
<span id="dateOfBirthDisplay">{{user.dateOfBirth}}</span>
</div>
The matching controller simply exposes the user
property via its $scope
and we have removed all interactions with jQuery UI from this business logic code:
function DemoController($scope) {
$scope.user = {
dateOfBirth: new Date(1970, 0, 1)
}
}
Creating the Directive
To create the link to jQuery UI's datepicker-addin, we are introducing the following directive. (Worry not: we'll discuss this directive line-by-line just in a few seconds.)
angular.module("demo", []).directive('myDatepicker', function ($parse) {
return {
restrict: "E",
replace: true,
transclude: false,
compile: function (element, attrs) {
var modelAccessor = $parse(attrs.ngModel);
var html = "<input type='text' id='" + attrs.id + "' >" +
"</input>";
var newElem = $(html);
element.replaceWith(newElem);
return function (scope, element, attrs, controller) {
var processChange = function () {
var date = new Date(element.datepicker("getDate"));
scope.$apply(function (scope) {
// Change bound variable
modelAccessor.assign(scope, date);
});
};
element.datepicker({
inline: true,
onClose: processChange,
onSelect: processChange
});
scope.$watch(modelAccessor, function (val) {
var date = new Date(val);
element.datepicker("setDate", date);
});
};
}
};
});
Contrary to the example which you've seen in Introduction to Directives, we're not simply returning alink
function. Instead, we're returning a so called compile
function for this directive.
The Compile Function
The reason for this is based on the internal logic in which directives are applied by Angular: in the first phase, the compile phase, you can modify the HTML-element which will be added to the DOM at the location of your directive. You can for example emit a completely different HTML element. In the second phase, during linking, you can change the content and behavior of the element after the result of the compilation has been added to the DOM.
The important thing to note is that, if your directive uses a compile
function, it is required to return thelink
function which should be called at a later time.
So let's look at the individual elements of our directive, step by step.
First, we're defining a module demo (which we will later reference from the HTML) and we're adding a directive called myDatepicker to it. We require a reference to $parse
to be injected in the code so that we can later use this to parse model-binding expressions.
angular.module("demo", [])
.directive('myDatepicker', function ($parse) {
We then indicate that our directive will be used as an HTML-elements (and not as an attribute or CSS-class).
restrict: "E"
We then tell AngularJS that we want to replace the element with the result of our directive but don't require automatic transclusion of the content. (You can read more about transclusion at Introduction to Directives).
replace: true,
transclude: false,
The remainder of the directive is the compile function. Let's first look at the compilation inside this function. At the beginning of this method, we use $parse
to parse the ng-model
attribute which has been specified in the HTML markup (it contains the target field for data binding).
$parse
converts (or compiles) an AngularJS expression into a function. If we for example specify "user.dateOfBirth" as an expression, then $parse
will return a function which allows us to retrieve and set this value from the underlying scope. (Please note: as always with AngularJS, the naming convention islowercase-with-dashes in HTML and camelCase in JS, so that the JavaScript field attrs.ngModel
will contain the string-contents of the HTML-attribute ng-model
).
compile: function (element, attrs) {
var modelAccessor = $parse(attrs.ngModel);
After this, we use jQueryOnly (or the AngularJS-supplied minimum jQuery equivalent) to create an<input>
-element and then replace the existing directive element (which temporarily exists in the HTML) with this new <input>
. (And while doing this, we're preserving the HTML element's ID from the HTML element which defined this directive.)
var html = "<input type='text' id='" + attrs.id + "' >" +
"</input>";
var newElem = $(html);
element.replaceWith(newElem);
The Link Function
Up to this point, we've looked only at the compilation part. What's next is that we're returning the link
function from this compile
function:
return function (scope, element, attrs, controller) {
/* ... */
};
In this returned link
-function, we're doing three things:
- define a function which will be called as the datepicker's
onClose
andonSelect
callbacks. - use jQuery UI's
datepicker()
extension to add the desired behavior to the underyling<input>
-element - watch the model for changes to update the datepicker's display whenever the model changes
Let's first have a look at the function which will later be used for the datepicker's callbacks.
var processChange = function () {
var date = new Date(element.datepicker("getDate"));
scope.$apply(function (scope) {
modelAccessor.assign(scope, date);
});
};
The important parts here happen in the call to scope.$apply
. This method is used to change model values in a way which allows AngularJS' change tracking to update the underlying HTML elements. (Or more correctly, it triggers the watches
which have been registered for the model).
The modelAccessor
we're using in this method is the return value of the earlier call to $parse(...)
: its assign
method is a preprocessed accessor function which sets the configured (according to the ng-model
attribute on the HTML directive) field of the corresponding object to the specified value.
To recap: whenever this processChange
-function is called, it will extract the current value from the HTML element which underpins the datepicker by using its getDate
access-method. It will then apply the changes to the underlying model by using the assign()
method of a pre-parsed accessor within an $apply
-block.
In the next step, we simply use jQuery UI's datepicker extension to get the desired client-side behavior. We pass the processChange
- function which we've defined above as the callbacks for onClose
and onSelect
. In this way processChanges
will be called whenever the user changes the datepicker's value.
element.datepicker({
inline: true,
onClose: processChange,
onSelect: processChange
});
And as the final step, we watch the model for changes so that we can update the UI as required. To do this, we pass the modelAccessor
(which we've received as the return value from $parse
) to Angular's $watch method, which is part of the current scope
.
The callback to $watch
will be triggered whenever the model value has been changed. (By using $apply
or by updating a bound UI control).
scope.$watch(modelAccessor, function (val) {
var date = new Date(val);
element.datepicker("setDate", date);
});
The HTML
In this way, we have now defined a re-usable directive which works independent from any GUI. In fact, this component could easily be re-used in a completely different AngularJS application.
If you run this initial version of the AngularJS application, you can see the following behavior:
At this point, the data-binding works as expected, we have a very clear separation of concerns (there is absolutely no direct binding between your business code in the Controller and jQuery UI's datepicker.)
This was a preview-quality chapter of our continuously deployed eBook on AngularJS for .NET developers. If you enjoyed this chapter, you can read more about the project at http://henriquat.re. You can also follow us on twitter (Project: @henriquatreJS, Authors: @ingorammer and @christianweyer)
(source : http://henriquat.re/directives/advanced-directives-combining-angular-with-existing-components-and-jquery/angularAndJquery.html)