Getting your development environment set up
Building
Testing
Best Practices
...
Brown Bag Presentation
Meet AngularJS!
Data Binding and Responding to Scope Changes
This example demonstrates how to react on a model change to trigger some further actions. The value greeting will be changed whenever there’s a change to the name model and the value is not blank.
Code Block |
---|
|
<html>
<head>
<script src="js/angular.js"></script>
<script src="js/app.js"></script>
<link rel="stylesheet" href="css/bootstrap.css">
</head>
<body ng-app>
<div ng-controller="MyCtrl">
<input type="text" ng-model="name" placeholder="Enter your name">
<p>{{greeting}}</p>
</div>
</body>
</html>
|
Code Block |
---|
|
function MyCtrl($scope) {
$scope.name = "";
$scope.$watch("name", function(newValue, oldValue) {
if (newValue.length > 0) {
$scope.greeting = "Greetings " + newValue;
}
});
}
|
About Angular
- Open source project hosted on GitHub and licensed by Google, Inc. under the terms of the MIT license.
- AngularJS is a relatively new actor on the client-side MVC frameworks scene, yet it has managed to attract a lot of attention, mostly due to its innovative templating system, ease of development, and very solid engineering practices
- Prescriptive Framework - AngularJS is prescriptive in the sense that it has a recommended way of building web apps. It has its own spin on the ubiquitous MVC pattern that is especially well suited for JavaScript.
How does it stand out from the crowd?
- Automatic refresh and the two-way data binding frees developers from the tedious work of explicitly triggering UI repaints
- Live DOM generated from HTML syntax is used as a templating language. More importantly, it is possible to extend an existing HTML vocabulary (by creating new directives), and then build UIs using a new HTML-based DSL
- Declarative approach to the UI results in a very concise and expressive way
- The excellent UI templating machinery doesn't put any constraints on the JavaScript code (for example, models and controllers can be created using plain, old JavaScript without the AngularJS APIs called all over the place)
Core Features
Dependency Injection
- The built-in support for DI makes it easy to assemble a web application from smaller, thoroughly tested services
Two-way Data Binding
- It makes it easy to change a value and effortlessly have it update the DOM. This means that there is only one place in your application where that piece of information is stored
Strong Focus on Testability
- The design of the framework and the tooling around it promote testing practices at each stage of the development process.
Deep Linking
- The framework makes it easy to share and change the application state. The URL of the application reflects what the user is doing.
Unique Templating System
- It uses HTML as the templating language
- It doesn't require an explicit DOM refresh, as AngularJS is capable of tracking user actions, browser events, and model changes to figure out when and which templates to refresh
- It has a very interesting and extensible components subsystem, and it is possible to teach a browser how to interpret new HTML tags and attributes (Directives!!!)
- You can create custom DOM elements, attributes, or classes that attach functionality that you define in JavaScript
Core concepts
Modules
Modules exist as containers that can provide configuration information, runtime dependencies, and other infrastructure support.
An Angular (main) "application" is just a module except...
- it should know about all the other modules
- it is the one identified by the ngApp directive
With Angular you can place all your controllers into a one module and all your services into a second module, or put the controllers and services all inside a single module
- Advantages of using multiple modules in one app:
- Keeps our global namespace clean
- Eases writing tests as well as keeps them clean as easy to target isolated functionality
- Eases to share code between applications
- Allows different parts of the code to be loaded in any order
Scopes
- Scopes objects that contain functionality and data to use when rendering the view. It is the single source of truth for all views. You can think of scopes as view models.
- Basic Functions
- They provide observers to watch for model changes
- They provide the ability to propagate model changes through the application as well as outside
the system to other components - They can be nested such that they can isolate functionality and model properties.
- They provide an execution environment in which expressions are evaluated.
- All properties found on the $scope object are automatically accessible to the view.
Scope Inheritance
- $scopes in AngularJS are arranged in a hierarchical structure that mimics the DOM and thus are nestable and we can reference properties on parent $scopes.
- Every part of an AngularJS application has a parent scope (at the ng-app level, this is called the $rootScope). All scopes are created with prototypal inheritance, meaning that they have access to their parent scopes.
- By default, for any property that AngularJS cannot find on a local scope, AngularJS will crawl up to the containing (parent) scope (and so on and so forth until it reaches the $rootScope) and look for the property or method there.
In this example, we can then reference data on the ParentController’s containing $scope on the child scope.
Code Block |
---|
|
<div ng-controller="ParentController">
<div ng-controller="ChildController">
<a href="#" ng-click="sayHello()">Say hello</a>
</div>
{{ person }}
</div>
|
Code Block |
---|
|
var app = angular.module('myApp', []);
app.controller('ParentController', function($scope) {
$scope.person = { greeted: false };
})
app.controller('ChildController', function($scope) {
$scope.sayHello = function() {
$scope.person.name = "Chuck Norris";
$scope.person.greeted = true;
}
})
|
Controllers
- The controller in AngularJS is a function that adds additional functionality to the scope of the view. They are used to set up an initial state and to add custom behavior to the scope object.
- When a new controller is created on a page, it is passed a new $scope generated by Angular.
- To create custom actions we can call in our views, we can just create functions on the scope of the controller.
Code Block |
---|
|
var app = angular.module('myApp', []);
app.controller('MyController', ['$scope', function($scope) {
$scope.counter = 0;
$scope.add = function(amount) {
$scope.counter += amount;
};
$scope.subtract = function(amount) {
$scope.counter -= amount;
};
}]);
|
One major difference between other JavaScript frameworks and AngularJS is that the controller is not the appropriate place to do any DOM manipulation or formatting, data manipulation, or state maintenance beyond holding the model data. It is simply the glue between the view and the $scope model.
Services
- Services provide a method for us to keep data around for the lifetime of the app and communicate across controllers in a consistent manner.
- Services are singletons objects that are instantiated only once per app (by the $injector) and lazy-loaded (only created when necessary). They provide an interface to keep together methods that relate to a specific function.
The most common and flexible way to create a service uses the angular.module API factory:
Code Block |
---|
|
// Example service that holds on to the current_user for the lifetime of the app
angular.module('myApp.services', [])
.factory('UserService', ['$http', function($http) {
var current_user;
return {
getCurrentUser: function() {
return current_user;
},
setUsername: function(user) {
current_user; = user;
}
}
}]);
|
Views
- The View is the HTML after it has been through the AngularJS compilation cycle
- 2 Step Process
- Compile Phase: AngularJS goes through the DOM and makes a note of all the directives by collecting all the link functions associated with each directive
- Link Phase: AngularJS injects the appropriate scope into each directive's link function setting up two-way data binding completing the link between scope and DOM.
Expressions
- {{ }} notation in the view
- Expressions are similar to like the result of an eval(javascript) (roughly). They are processed by Angular and, therefore, have these important, distinct properties:
- All expressions are executed in the context of the scope and have access to local $scope variables.
- They do not throw errors if an expression results in a TypeError or a ReferenceError.
- They do not allow for any control flow functions (conditionals; e.g., if/else).
- They can accept a filter and/or filter chains
- To manually parse an expression, we can inject the $parse service into a controller and call the service to do the parsing (use $interpolate to parse a string with multiple expressions)
Filters
Angular Filters are typically used to format expressions in bindings in your template. They transform the input data to a new formatted data type. Filters are invoked in the HTML with the | (pipe) character in the template binding.
Built-in filter examples:
Code Block |
---|
|
<div>{{ name | uppercase }}</div>
<div>{{ 123.456789 | number:2 }}</div>
<div>{{ 123 | currency }}</div>
<div>{{ today | date:'medium' }}</div>
<div>{{ today | date:'short' }}</div>
<div>{{ today | date:'fullDate' }}</div>
<div>{{ today | date:'longDate' }}</div>
<div>{{ today | date:'mediumDate' }}</div>
<div>{{ today | date:'shortDate' }}</div>
<div>{{ today | date:'mediumTime' }}</div>
<div>{{ today | date:'shortTime' }}</div>
<div>Month in year: {{ today | date:'MMMM' }}</div>
<div>Short month in year: {{ today | date:'MMM' }}</div>
<div>Padded month in year: {{ today | date:'MM' }}</div>
<div>Month in year: {{ today | date:'M' }}</div>
<div>{{ ['Ari', 'Lerner', 'Likes', 'To', 'Eat', 'Pizza'] | filter:'e' }}</div>
<div>
{{ [{
'name': 'Ari',
'City': 'San Francisco',
'favorite food': 'Pizza'
}, {
'name': 'Nate',
'City': 'San Francisco',
'favorite food': 'indian food'
}] | filter:{'favorite food': 'Pizza'} }}
</div>
|
Implementing a Custom Filter to Reverse an Input String:
Code Block |
---|
|
<body ng-app="MyApp">
<input type="text" ng-model="text" placeholder="Enter text"/>
<p>Input: {{ text }}</p>
<p>Filtered input: {{ text | reverse }}</p>
</body>
|
Code Block |
---|
|
var app = angular.module("MyApp", []);
// Create custom filter
app.filter("reverse", function() {
return function(input) {
var result = "";
input = input || "";
for (var i=0; i<input.length; i++) {
result = input.charAt(i) + result;
}
return result;
};
});
|
Advanced Concepts
Directives
Directives are Angular’s method of creating new HTML elements with their own custom functionality. For instance, we can create our own custom element "my-directive"
Code Block |
---|
|
angular.module('BrownBagDemoApp')
.directive('myDirective', function () {
return {
restrict: 'E',
template: '<a href="http://pentaho.com">Click me to go to Pentaho</a>'
};
});
|
Digest Cycle
How does Angular know when something has changed and it's time to update the bindings??
- AngularJS works on a concept of dirty checking: a simple process of comparing a value with its previous value and if it has changed then a change event is fired.
- Dirty checking is performed via a digest cycle that is controlled by $digest service
- During the compilation phase, $scope evaluates all of its properties and creates a watchExpression for each one.
- When a watchExpression detects that a $scope property has changed then a listener function is fired.
Dependency Injection
- Implicit: Angular will assume the function parameter names are the names of the dependencies. It evaluates function signatures using the toString() method and then see if there is a matching service available.
Note: that this will only work with non-minified/non-obfuscated code as angular needs to parse the arguments in-tact.
- Explicit: Use the $inject annotation to define what is going to be injected.
Note: This method allows for minifiers to rename the function parameters and still be able to inject the proper services into the function.
- Inline: Allows us to pass an array of arguments instead of a function when defining an angular object. The elements inside this array are the list of injectable dependencies as strings, with the last argument being the function definition of the object.
**Note: This will also work with minified/obfuscated code
Code Block |
---|
|
// Implicit
angular.module('myApp')
.controller('MyCtrl', function($scope, myService) {
// Do something awesome
});
// Explicit
var MyCtrl = function(some$scope, someService) {
// Do something awesome
}
MyCtrl.$inject = ['$scope', 'myService'];
// Inline
angular.module('myApp')
.controller('MyCtrl', ['$scope', 'myService',
function($scope, myService) {
// Do something awesome
}]);
|
Routing Services
- The AngularJS framework has a built-in $route service that can be configured to handle route transitions in single-page web applications
- Routes can be defined during the application's configuration phase using the $routeProvider service
Code Block |
---|
|
angular.module("MyApp", []).
config(function($routeProvider, $locationProvider) {
$routeProvider
.when("/persons", {
templateUrl: "partials/index.html"
})
.when("/persons/:id", { // In ShowCtrl, id is available via $routeParams.id
templateUrl: "partials/show.html",
controller: "ShowCtrl"
})
.when("/login", {
templateUrl: "partials/login.html",
controller: "LoginCtrl"
})
.when("/help", {
templateUrl: "partials/help.html"
})
.otherwise( {
redirectTo: "/persons"
});
});
|
Testing
Unit Test with Jasmine
- Jasmine is a Behavior-driven development framework for testing JavaScript code
- Karma supports multiple testing frameworks, but the default option is Jasmine
Anatomy of a Jasmine Test
Code Block |
---|
|
describe('hello World test', function () {
var greeter;
beforeEach(function () {
greeter = new Greeter();
});
it('should say hello to the World', function () {
expect(greeter.say('World')).toEqual('Hello, World!');
});
});
|
- The describe function, describes a feature of an application. It acts as an aggregating container for individual tests (test suite)
- Code contained in the beforeEach block will get executed before each individual test
- The test itself is located inside the it function. The idea is to exercise one particular aspect of a feature in one test
Testing AngularJS Objects with Jasmine
We need to ensure that a proper AngularJS module is initialized and the whole DI machinery is configured
Code Block |
---|
|
describe('UserService Service', function() {
// indicate that services from a given module should be prepared for the test
// $injector should be created for a given module (and all the dependent modules).
beforeEach(module('services'));
var userService;
beforeEach(inject(function(UserService) { // inject UserService and assign to local variable
userService = UserService;
}));
// execute test
it('should have user', function() {
expect(userService.getUser()).toEqual('testUser');
});
});
|
Best Practices
There are some great resources available on this topic already. Instead of duplicating all of them, here are some links to review.
However, it does makes sense reiterate some of the most relevant ideas as well as a few that apply to Pentaho-specific development.
- If you find yourself manipulating DOM in a controller, STOP!!! DOM manipulation should really only be done in Directives and possibly in Filters.
- Keep Filters lean and mean. They get executed many times and can become a performance issue.
- Use angular-supplied versions of common javascript/jquery methods/objects
- Get to know the Angular API, don't rely on jQuery
General
- AngularJS is not suited for applications with a strong graphical component: ones that do many different heavy DOM modifications or games.
- Do not attempt to write a general-purpose JavaScript library in AngularJS; it's meant for writing applications, not libraries. You can write modules to be reused by other AngularJS applications, but anything intended to be standalone or used with another library is not a good candidate.
App Structure
- Stick to the one file equals one AngularJS module principle. This will allow you to maintain relatively small, focused files and modules. Additionally you won't be concerned with the load order of those files. Also it will be possible to load individual modules under unit tests.
Scopes
- The $rootScope object is the closest object we have to the global context in an angular app. It’s a bad idea to attach too much logic to this global context, just like it’s not a good idea to dirty the javascript global scope.
- A model refers to a traditional JavaScript object {} where transient state should be stored. Persistent state should be bound to a service, which is then responsible for dealing with persisting that model.
Data Binding
- Due to the nature of javascript itself and how it passes by value vs. reference, it’s considered a best- practice in Angular to bind references in the views by an attribute on an object, rather than the raw object itself.
Services
- If you want your function to be called like a normal function, use factory. If you want your function to be instantiated with the new operator, use service. If you don't know the difference, use factory. Service or Factory?
Controllers
- Create variables inside the controller to be explicit.
- It is considered a best-practice to name our controllers NameCtrl in camelcase.
- Keep controllers slim by using the dependency injection feature of AngularJS to access services.
- Controllers should not own the domain model. If something is needed by two controllers then make it available via a service.
Dependency Injection
- Implicit dependency injection works fine until you minimize your code and then it falls apart. For production use, it is recommended to explicitly annotate injections with $inject.
Directives
- While declaring an AngularJS directive, the naming convention followed is camelCase. For example, we would define the name of the directive as 'myDatePicker'. But when you actually use the directive in your HTML, it is the dash-separated version (my-date-picker).
- To make your directives and attributes HTML5 compliant, you can prefix all AngularJS attributes with “data-” or “x-”. That is, “x-ng-show” or “data-date-picker”, and AngularJS will treat it just like you wrote it like a normal HTML element.
Spreading route definitions among several modules
In a large scale web application, it is not uncommon to have dozens and dozens of different routes. While the $routeProvider service offers a very nice, fluent style API, route definitions can get quite verbose (especially when the resolve property is used). If we combine a large number of routes with the relative verboseness of each definition, we quickly end up maintaining a huge JavaScript file with several hundreds of lines of code! Worse yet, having all routes defined in one huge file means that this file must be modified quite frequently by all developers working on a project – a recipe for creating a bottlenecks and disastrous merge issues.
With AngularJS, we are not forced to define all the routes in one central file! If we take the approach of modularizing each functional area (has its own dedicated module), we can move routes linked to a certain part of an application to the corresponding module.
In the AngularJS module system, each and every module can have its associated config function, where we can inject $routeProvider service and define routes. For example: each of these submodules defines its own routes as follows:
Code Block |
---|
|
angular.module('admin-users', [])
.config(function ($routeProvider) {
$routeProvider.when('/admin/users', {...});
$routeProvider.when('/admin/users/new', {...});
$routeProvider.when('/admin/users/:userId', {...});
});
angular.module('admin-projects', [])
.config(function ($routeProvider) {
$routeProvider.when('/admin/users', {...});
$routeProvider.when('/admin/users/new', {...});
$routeProvider.when('/admin/users/:userId', {...});
});
angular.module('admin', ['admin-projects', 'admin-users']);
|
Pentaho Platform Integration
Plugging into Pentaho User Console
A seed project has been created to demonstrate the usage of the PUC Plugin API. The REAMDME on the Github page describes how to use it. https://github.com/pentaho/pentaho-angular-seed