One of the best ways to learn something new is to see how the things you already know are used in it. This document does not intend to make its readers familiar with the design or architectural patterns; it suggests basic understanding of the concepts of the OOP, design patterns and architectural patterns. The goal of this paper is to describe how different software design and architectural patterns are applied in AngularJS or any AngularJS single-page application.
The document begins with brief overview of the AngularJS framework. The overview explains the main AngularJS components - directives, filters, controllers, services, scope. The second section lists and describes different design and architectural patterns, which are implemented inside the framework. The patterns are grouped by the AngularJS component they are used in. If some patterns are used inside multiple components it will be explicitly mentioned.
The last section contains a few architectural patterns, which are commonly used inside most of the single-page applications built with AngularJS.
AngularJS is a JavaScript framework developed by Google. It intends to provide a solid base for the development of CRUD Single-Page Applications (SPA). SPA is a web application, which once loaded, does not require full page reload when the user performs any actions with it. This means that all application resources (data, templates, scripts, styles) should be loaded with the initial request or better - the information and resources should be loaded on demand. Since most of the CRUD applications has common characteristics and requirements, AngularJS intends to provide the optimal set of them out-of-the-box. A few important features of AngularJS are:
The separation of concerns is achieved by dividing each AngularJS application into separate components, such as:
These components can be grouped inside different modules, which helps to achieve a higher level of abstraction and handle complexity. Each of the components encapsulates a specific piece of the application's logic.
The partials are HTML strings. They may contain AngularJS expressions inside the elements or their attributes. One of the distinctions between AngularJS and the others frameworks is the fact that AngularJS' templates are not in an intermediate format, which needs to be turned into HTML (which is the case with mustache.js and handlebars, for example).
Initially each SPA loads index.html file. In the case of AngularJS this file contains a set of standard and custom HTML attributes, elements and comments, which configure and bootstrap the application. Each sub-sequenced user action requires only load of another partial or change of the state of the application, for example through the data binding provided by the framework.
Sample partial
<html ng-app>
<body ng-controller="MyController">
<input ng-model="foo" value="bar">
<button ng-click="changeFoo()">{{buttonText}}</button>
<script src="https://github.com/mgechev/angularjs-in-patterns/raw/main/angular.js"></script>
</body>
</html>
With AngularJS expressions partials define what kind of actions should be performed for handling different user interactions. In the example above the value of the attribute ng-click states that the method changeFoo of the current scope will be invoked.
The AngularJS controllers are JavaScript functions, which help handling the user interactions with the web application (for example mouse events, keyboard events, etc.), by attaching methods to the scope. All required external, for the controllers, components are provided through the Dependency Injection mechanism of AngularJS. The controllers are also responsible for providing the model to the partials by attaching data to the scope. We can think of this data as view model.
function MyController($scope) {
$scope.buttonText = 'Click me to change foo!';
$scope.foo = 42;
$scope.changeFoo = function () {
$scope.foo += 1;
alert('Foo changed');
};
}
For example, if we wire the sample controller above with the partial provided in the previous section the user will be able to interact with the application in few different ways.
foo by typing in the input box. This will immediately reflect the value of foo because of the two-way data binding.foo by clicking the button, which will be labeled Click me to change foo!.All the custom elements, attributes, comments or classes could be recognized as AngularJS directives if they are previously defined as ones.
In AngularJS scope is a JavaScript object, which is exposed to the partials. The scope could contain different properties - primitives, objects or methods. All methods attached to the scope could be invoked by evaluation of AngularJS expression inside the partials associated with the given scope or direct call of the method by any component, which keeps reference to the scope. By using appropriate directives, the data attached to the scope could be binded to the view in such a way that each change in the partial will reflect a scope property and each change of a scope property will reflect the partial.
Another important characteristics of the scopes of any AngularJS application is that they are connected into a prototypical chain (except scopes, which are explicitly stated as isolated). This way any child scope will be able to invoke methods of its parents since they are properties of its direct or indirect prototype.
Scope inheritance is illustrated in the following example:
<button id="parent-method" ng-click="foo()">Parent method</button>
<button ng-click="bar()">Child method</button>
function BaseCtrl($scope) {
$scope.foo = function () {
alert('Base foo');
};
}
function ChildCtrl($scope) {
$scope.bar = function () {
alert('Child bar');
};
}
With div#child is associated ChildCtrl but since the scope injected inside ChildCtrl inherits prototypically from its parent scope (i.e. the one injected inside BaseCtrl) the method foo is accessible by button#parent-method.
In AngularJS the directives are the place where all DOM manipulations should be placed. As a rule of thumb, when you have DOM manipulations in your controller you should create a new directive or consider refactoring of already existing one, which could handle the required DOM manipulations. Each directive has a name and logic associated with it. In the simplest case the directive contains only name and definition of postLink function, which encapsulates all the logic required for the directive. In more complex cases the directive could contain a lot of properties such as:
By citing the name of the directives they can be used inside the declarative partials.
Example:
myModule.directive('alertButton', function () {
return {
template: '<button ng-transclude></button>',
scope: {
content: '@'
},
replace: true,
restrict: 'E',
transclude: true,
link: function (scope, el) {
el.click(function () {
alert(scope.content);
});
}
};
});
<alert-button content="42">Click me</alert-button>
In the example above the tag <alert-button></alert-button> will be replaced button element. When the user clicks on the button the string 42 will be alerted.
Since the intent of this paper is not to explain the complete API of AngularJS, we will stop with the directives here.
The filters in AngularJS are responsible for encapsulating logic required for formatting data. Usually filters are used inside the partials but they are also accessible in the controllers, directives, services and other filters through Dependency Injection.
Here is a definition of a sample filter, which changes the given string to uppercase:
myModule.filter('uppercase', function () {
return function (str) {
return (str || '').toUpperCase();
};
});
Inside a partial this filter could be used using the Unix's piping syntax:
{{ name | uppercase }}
Inside a controller the filter could be used as follows:
function MyCtrl(uppercaseFilter) {
$scope.name = uppercaseFilter('foo'); //FOO
}
Every piece of logic, which doesn't belong to the components described above, should be placed inside a service. Usually services encapsulate the domain specific logic, persistence logic, XHR, WebSockets, etc. When the controllers in the application became too "fat" the repetitive code should be placed inside a service.
myModule.service('Developer', function () {
this.name = 'Foo';
this.motherLanguage = 'JavaScript';
this.live = function () {
while (true) {
this.code();
}
};
});
The service could be injected inside any component, which supports dependency injection (controllers, other services, filters, directives).
function MyCtrl(Developer) {
var developer = new Developer();
developer.live();
}
In the next a couple of sections, we are going to take a look how the traditional design and architectural patterns are composed in the AngularJS components.
In the last chapter we are going to take a look at some architectural patterns, which are frequently used in the development of Single-Page Applications with (but not limited to) AngularJS.
The singleton pattern is a design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system. The concept is sometimes generalized to systems that operate more efficiently when only one object exists, or that restrict the instantiation to a certain number of objects.
In the UML diagram bellow is illustrated the singleton design pattern.
When given dependency is required by any component, AngularJS resolves it using the following algorithm:
$get). Note that instantiating the dependency may require recursive call to the same algorithm, for resolving all the dependencies required by the given dependency. This process may lead to circular dependency.We can take better look at the AngularJS' source code, which implements the method getService:
```JavaScript function getService(serviceName) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; } throw err; } finally {
$ claude mcp add angularjs-in-patterns \
-- python -m otcore.mcp_server <graph>