An AngularJS app is a self-contained single page JavaScript app that lives in its own folder. Three important app subfolders are referenced in this discussion:
directives-folder
: normally directives
controllers-folder
: normally controllers
views-folder
: normally views
Reference is also made to index.html
which lives in the app root.
Custom UI components can be created in AngularJS through a combination of a directive and a controller. The directive contains the HTML template while the controller is JS script that provides access to data and computation. With this arrangement, a directive can use data binding to access data from its controller. The appearance of the component is created through its directive definition, including the location and appearance of bound data inside the component. The functionality of the component, including scoped values or computations, is handled by the controller.
In AngularJS, a view is a container for displaying static HTML along with custom components. The view is rendered inside the <ng-view>
div in index.html
. Which view appears inside the div at any one time is determine by routing, an examination of the URL parameters following the /
. In the example app, routes are defined in controller-folder/mimix.routing.js
.
Directive HTML templates can bind to many kinds of data. This walkthrough will demonstrate 2 types:
Let's assume a routing setup that sends home page visitors to a welcomeView.html
in the view-folder
. That file contains a reference to one existing component:
<quoted-section quote="SBCL Test"></quoted-section>
We're going to add new component <msl-communicator>
, starting by referring to it here in the view. It won't have any effect on the view until it's wired-up to a directive and a controller in the next steps.
<quoted-section quote="SBCL Test"></quoted-section>
<msl-communicator request="(@WALT Walt Disney)"></msl-communicator>
Note that, by convention in AngularJS, directives and functions are named with camelCase names like
mslCommunicator
but invoked in HTML with dash-delimited element names like<msl-communicator>
. While there is no requirement that controllers share the name of the directives that use them (and multiple AngularJS directives can use the same controllers), by convention in Mimix code only one controller is defined for each directive and it has the same name,directiveName
in these examples.
The controller provides data and computation for the component. Each controller is in its own file named directiveName.js
in the controllers-folder
, so the existing controller for the quotedSection
directive can be found in quotedSection.js
.
We'll start with the contents of quotedSection.js
and save it as a new file mslCommunicator.js
in the same folder:
// Usage:
// <quoted-section quote="It was a dark and stormy night..."></quoted-section>
//Controller Definition
var quotedSectionFunction = function ($routeParams, $attrs) {
//SCOPED DATA
this.quote = $attrs.quote;
this.version = CONSTANT.version;
};
//Dependency Injection
quotedSectionFunction.$inject = ["$routeParams", "$attrs"];
//Controller Assignment
ungallery.controller("quotedSection",quotedSectionFunction);
Before changing the code, we can paste in the component invocation from the view as an example of the syntax we want to support. We'll eventually remove the other example, <quoted-section>
, but let's leave it for now so we can see how the invocation relates to the controller syntax.
Usage:
// <quoted-section quote="It was a dark and stormy night..."></quoted-section>
// <msl-communicator request="(@WALT Walt Disney)"></msl-communicator>
The definition function creates the scope of the controller. Variables defined here are available to the component for data binding. The $routeParams
dependency allows access to the URL or Angular route. The $attrs
dependency allows access to the attributes of the component at invocation time, like quote
or request
.
The convention of naming the function directiveNameFunction
helps mentally associate the controller with the directive without confusing the two in Angular code.
//Controller Definition
var mslCommunicatorFunction = function ($routeParams, $attrs) {
//SCOPED DATA
this.quote = $attrs.quote;
this.version = CONSTANT.version;
};
Inside the function, values can be assigned to the this
object and accessed later with data binding. The example collects two values from the runtime environment: $attrs.quote
which was the value of @quote
in the previous component, and CONSTANT.version
which is an inherited global value setup in the app's startup code.
Let's modify quote
to use the attribute we want from <msl-communicator>
, which is request
:
//SCOPED DATA
this.request = $attrs.request;
Some AngularJS features such as access to routing or attributes require the injection of dependencies. The name of the function must match the directiveNameFunction
and the list of dependencies must match the parameters in the definition of directiveNameFunction
, in this case $routeParams
and $attrs
. Here, the directiveNameFunction
has been updated to mslCommunicatorFunction
:
//Dependency Injection
mslCommunicatorFunction.$inject = ["$routeParams", "$attrs"];
To complete the wiring, you must assign the controller to the directive by calling the controller
function on your AngularJS appname
with two parameters. The first, directiveName
, is the quoted name of the directive. The second parameter, directiveNameFunction
is an unquoted reference to the controller definition.
In this example, appname
is ungallery
, directiveName
is "mslCommunicator"
, and directiveNameFunction
is mslCommunicatorFunction
:
//Controller Assignment
ungallery.controller("mslCommunicator",mslCommunicatorFunction);
The finished controller definition mslCommunicator.js
looks like this:
// Usage:
// <msl-communicator request="(@WALT Walt Disney)"></msl-communicator>
//Controller Definition
var mslCommunicatorFunction = function ($routeParams, $attrs) {
//SCOPED DATA
this.request = $attrs.request;
this.version = CONSTANT.version;
};
//Dependency Injection
mslCommunicatorFunction.$inject = ["$routeParams", "$attrs"];
//Controller Assignment
ungallery.controller("mslCommunicator",mslCommunicatorFunction);
Since our app keeps controllers in separate files in the controller-folder
, we need to add a line of <script>
to index.html
ensure that this new controller is loaded at runtime. Here, controllers-folder
is controllers/source/
and directiveName
is mslCommunicator
.
<!-- Controllers -->
<script type="text/javascript" src="controllers/source/mslCommunicator.js"></script>
The directive is the HTML template with access to inline databinding. Each directive is in its own file named directiveName.html
in the directives-folder
, so the existing template for the quotedSection
directive can be found in quotedSection.html
.
With AngularJS, it's often easier to copy and modify a working example. Here, we'll take the entire contents of quotedSection.html
and save it as a new directive called mslCommunicator.html
.
<section class="content-block content-2-4 min-height-400px bg-deepocean">
<div class="container text-center">
<i class="fa fa-4x fa-quote-left"></i>
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<div class="item">
<div class="editContent">
<h1>{{quotedSection.quote}}</h1>
</div>
<div class="editContent">
<h3 class="text-muted">v {{quotedSection.version}}</h3>
</div>
</div>
</div>
</div>
</div><!-- /.container -->
</section>
To be used inside a view or another component, the new directive must be declared to AngularJS. In the example app, directives are declared in controllers-folder/ungallery.directives.js
. Here is the existing declaration for the quotedSection
directive.
//quotedSection
ungallery.directive('quotedSection', function() {
return {
restrict: "E",
controller: "quotedSection",
controllerAs: "quotedSection",
templateUrl: 'directives/quotedSection.html'
}
});
To create one that works for our new directive, copy and paste this section into the same file and modify the copied section so that all occurrences of directiveName
are changed to your new directiveName
, in this case mslCommunicator
.
//mslCommunicator
ungallery.directive('mslCommunicator', function() {
return {
restrict: "E",
controller: "mslCommunicator",
controllerAs: "mslCommunicator",
templateUrl: 'directives/mslCommunicator.html'
}
});
The restrict: "E"
syntax restricts this component to being invoked as an element, rather than an attribute.
The controller
declaration here must match the first parameter provided to the call to appname.controller
in the controller definition file.
The controllerAs
declaration here binds the JavaScript this
object in the controller definition to the literal string mslCommunicator
, so that variables can be accessed through mslCommunicator.varName
syntax in the directive template.
The templateUrl
declaration tells where to find the directiveName.html
file.
There are several ways to access data and functions in the controller, but the simplest is to use the {{doubleCurlyBraces}}
syntax. To get the value of someVar
, just place {{someVar}}
inline with the HTML.
In order to prevent confusion about the meaning of the JavaScript this
object in scope (and to allow components to access each others' scopes), the this
object is reached by directiveName
as we assigned in the directive's declaration. We need to update both directiveName
and the name of the controller value we're trying reach in order to access the bound data.
In this example, we've updated directiveName
to mslCommunicator
and the value to request
, the variable that was defined in the Scoped Data section of mslCommunicator.js
.
<section class="content-block content-2-4 min-height-400px bg-deepocean">
<div class="container text-center">
<i class="fa fa-4x fa-quote-left"></i>
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<div class="item">
<div class="editContent">
<h1>{{mslCommunicator.request}}</h1>
</div>
<div class="editContent">
<h3 class="text-muted">v {{mslCommunicator.version}}</h3>
</div>
</div>
</div>
</div>
</div><!-- /.container -->
</section>
Refreshing the application's home page in a local browser (usually http://localhost:8000
) should show the original <quoted-section>
component along with the new <msl-communicator>
component underneath it. The new @request
value has been passed in and used along with the mslCommunicator.version
constant that was retrieved from the global scope.