3. Architecture

Angular is a framework for building client applications in HTML and either JavaScript or a language like Dart or TypeScript that compile/transpile to JavaScript.

The framework consists of several libraries, some of the core and some optional.

We write Angular applications by composing HTML templates with Angularized markup, writing component classes to manage those templates, adding application logic in services, and boxing components and services in modules.

Then we launch the app by bootstrapping the root module. Angular takes over, presenting our application content in a browser, like Chromium and responding to user interactions according to the instructions we have provided.

Of course there is more to it than this. We'll learn the details in the chapters that follow. For now, let's focus on the big picture.

Modules

Angular app are modular and Angular has its own modularity system called NgModules. NgModules are a big deal. This page introduces modules.

Every Angular app has at least one NgModule class, the root module, conventionally named AppModule.

While the root module may be the only module in a small application, most apps have many more feature modules, each a cohesive block of code dedicated to an application domain, a workflow, or a closely related set of capablities. An NgModule, whether a root or feature, is a class with an @NgModule decorator.

NgModule is a decorator function that takes a single metadata object whose properties describe the module.

  • declarations - the view classes that belong to this module. Angular has three kinds of view classes: components, directives and pipes.
  • exports - the subset of declarations that should be visible and usable in the component templates of other modules.
  • imports - other modules whose exported classes are needed by component templates declared in this module.
  • providers - creators of services that that this module contributes to the global collection of services; they become accessible in all parts of the app.
  • bootstrap - the main application view, called the root component, that hosts all other app views. Only the root modules should set this bootstrap property.

Here's our simple feature module:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { TechnologyListComponent } from './technology-list.component';
import { LoggerService } from './logger.service';

const routes: Routes = [
  { path: 'architecture', component: TechnologyListComponent }
];

@NgModule({
  imports: [ CommonModule, FormsModule, RouterModule.forChild(routes)],
  declarations: [ TechnologyListComponent ],
  providers: [ LoggerService ]
})
export class ArchitectureModule { }

We launch our application by bootstrapping the root module. During development we are likely to bootstrap the AppModule in a main.ts file like this one.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);

NgModules vs. JavaScript modules

The NgModule - a class decorated with @NgModule - is a fundamental feature of Angular.

JavaScript also has its own module system for managing collections of JavaScript objects. It's completely different and unrelated to the NgModule system.

In JavaScript each file is a module and all objects defined in the file belong to that module. The module declares some objects to be public by marking them with the export key word. Other JavaScript modules use import statements to access public objects from other modules.

import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

export class AppModule { }

Angular Libraries

Angular ships as a collection of JavaScript modules. We can think of them as library modules. Each Angular library name begins with the @angular prefix. We install them with the npm or yarn package manager and import part of them with JavaScript import statements.

For example we can import Angular's Component decorator from the @angular/core library like this:

import { Component } from '@angular/core;

We also import NgModules from Angular libraries using JavaScript import statements:

import { BrowserModule } from '@angular/platform-browser;

A root module needs material from within the BrowserModule. To access this material, we add it to the @NgModule metadata imports array like this:

  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    ReactiveFormsModule,
    FormsModule,
    HttpModule,
    DashboardModule,
    ArchitectureModule,
    AppRoutingModule,
    SharedModule,
    TechnologiesModule,
    LoginRoutingModule,
    QuestionsModule
  ],

This was we are using both the Angular and JavaScript module system together.

Components

A component controls a patch of the screen called a view.

For example, the following views are controlled by components:

  • The app root with the navigation links
  • The list of technologies
  • The technology editor

We define a component's application logic - what it does to support the view - inside a class. The class interacts with the view through an API of properties and methods.

For example the TechnologyListComponent has a technologies property the returns an array of technologies that it acquires from a service. TechnologyListComponent also has a selectTechnology() method that sets a selectedTechnology property when the user clicks to choose a technology from the list.

export class TechnologyListComponent implements OnInit {
      technologies: Technology[];
      selectedTechnology: Technology;

      constructor(private technologyService: TechnologyService) { }

      ngOnInit() {
        this.technologies = this.technologyService.getTechnologies();
      }

      selectTechnology(technology: Technology) {
        this.selectedTechnology = technology;
      }

}

Angular creates, updates and destroys components as the user moves through the application. Our app can take action at each moment in this lifecycle through optional lifecycle hooks, like ngOnInit() declared above.

Templates

We define a component's view with its companion template. A template is a form of HTML that tells Angular how to render the component. A template looks like regular HTML, except for a few differences. Here is a template for our TechnologyListComponent.

    <h2>Technology List</h2>
      <p><i>Pick a technology from the list</i></p>
      <ul>
          <li *ngFor="let technology of technologies" (click)="selectTechnology(technology)">
          {{ technology.name }}
          </li>
      </ul>
      <app-technology-detail *ngIf="selectedTechnology" [technology]="selectedTechnology">
      </app-technology-detail>

The template uses Angular's template syntax with code like *ngFor, interpolation with double curly braces, (click), [technology] and <app-technology-detail>.

In the last line of the template, the <app-technology-detail> tag is a custom element, an Angular component that represents TechnologyDetailComponent.

The TechnologyDetailComponent is a child of TechnologyListComponent. It presents facts about a particular Technology, the technology the user selects from a list prsented by the TechnologyListComponent.

Metadata

Metadata tells Angular how to process a class. To tell Angular that TechnologyListComponent is a component, we attach metadata to the class. We attach metadata by using a JavaScript decorator.

@Component({
  selector: 'app-technology-list',
  templateUrl: './technology-list.component',
  providers: [ TechnologyService ]
  `
})
export class TechnologyListComponent implements OnInit {
    /* ... */
}

The @Component decorator takes a required configuration object, with the information Angular needs to create and present the component and its view.

  • selector: CSS selector that tells Angular to create and insert an instance of this component where it finds a <app-technology-list> tag in parent HTML.
  • templateUrl: module relative address of this component's HTML template.
  • providers: array of dependency injection providers for services that the component requires.

The metadata in @Component tells Angular where to get the major building blocks we specify for the component. The template, metadata and component together describe a view. We apply other metadata decorators in a similar fashion to guide Angulars behavior. @Injectable, @Input and @Output are a few of the more popular decorators.

Data Binding

Without a framework, we would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push/pull logic by hand is tedious, error-prone, and a nightmare to read as any experienced jQuery programmer can attest.

There are four forms of data binding syntax. To the DOM, from the DOM, or in both directions.

    <li> {{ technology.name }}</li> <-- interpolation binding

    <app-technology-detail [technology]="selectedTechnology"></app-technology-detail> <-- property binding

    <li (click)="selectTechnology(technology)"></li> <-- event binding

    <input [(ngModel)]="technology.name"> <-- two-way binding

Angular processes all data bindings once per JavaScript event cycle, from the root of the application component tree through all child components.

Data binding plays an important role in communication between a template and its component.

Data binding is also important for communication between parent and child components.

Directives

Angular templates are dynamic. When Angular renders them, it transforms the DOM according to instructions given by the directives.

A directive is a class with a @Directive decorator. A component is a directive with a template; a @Component decorator is actually a @Directive decorator extended with template oriented features. Remember a component is technically a directive.

Two other kind of directives exist: structural and attribute directives.

Structural directives alter layout by adding, removing and replacing elements in DOM.

<li *ngFor="let technology of technologies"></li> <-- *ngFor tells Angular to stamp out one <li> element per technology in the technologies list
<app-technology-detail *ngIf="selectedTechnology"> <-- *ngIf includes the TechnologyDetail component only if a selected technology exists
</app-technology-detail>

Attribute directives alter the appearance or behavior of an existing element. For example ngSwitch, ngStyle and ngClass.

ngModel which implements two way data binding is also an example of an attribute directive.

<input [(ngModel)]="technology.name">

Services

Service is a broad category encompassing any value, function, or feature that our application needs. Almost anything can be a service. A service is typicallyy a class with a narrow, well-defined purpose. It should do something specific and do it well.

Here's an example of a service class that logs to the browser console.

import { Injectable } from '@angular/core';

@Injectable()
export class LoggerService {
  log(message: any) { console.log(message); }
  error(message: any) { console.error(message); }
  warn(message: any) { console.warn(message); }

}

Here's a TechnologyService that uses a Promise to fetch technologies.

import { Injectable } from '@angular/core';

import { Technology } from './technology';
import { BackendService } from './backend.service';
import { LoggerService } from './logger.service';

@Injectable()
export class TechnologyService {
  private technologies: Technology[] = [];

  constructor(private backendService: BackendService, private loggerService: LoggerService) { }

  getTechnologies() {
    this.backendService.getAll(Technology).then((technologies: Technology[]) => {
        this.loggerService.log(`Fetched ${technologies.length} technologies.`);
        this.technologies.push(...technologies); // fill cache
    });
    return this.technologies;
  }

}

BackendService.

import { Injectable, Type } from '@angular/core';

import { LoggerService } from './logger.service';
import { Technology } from './technology';

const TECHNOLOGIES = [
      new Technology('Angular 5', 'Data Binding, Routing, Dependency Injection, Animations'),
      new Technology('Angular CLI', 'Tool, Scaffolding, Maintaining, Bundling, Building Angular Applications'),
      new Technology('Angular Material', 'Beautiful Responsive CSS, Optimized for Angular')
];

@Injectable()
export class BackendService {

  constructor(private loggerService: LoggerService) { }

  getAll(type: Type<any>): PromiseLike<any[]> {
    if (type === Technology) {
      return Promise.resolve<Technology[]>(TECHNOLOGIES);
    }
    const err = new Error('Cannot get object of this type');
    this.loggerService.error(err);
    throw err;
  }

}

Dependency Injection

Dependency injection is a way to supply a new instance of a class with the fully formed dependencies it requires. Most dependencies are services. Angular uses dependency injection to provide the new components with the services they need. Angular can tell which services a component needs by looking at the types of its constructor parameters. For example, the constructor of our TechnologyListComponent needs a TechnologyService.

constructor(private technologyService: TechnologyService) { }

When Angular creates a component, it first asks an injector for the services that the component requires.

An injector maintains a container of service instances that it has previously created. If a requested service instance is not in the container, the container makes one and adds it to the container before returning the service to Angular. When all requested services have been resolved and returned, Angular can call the component's constructor with those services as arguments. This is dependency injection.

The process of TechnologyService injection looks a bit like this.

We must register a provider of the TechnologyService with the injector. A provider is something that can create or return a service, typically the service class itself.

We can register providers in modules or components. In general we add providers to the root or feature module so that the same instance of a service is available everywhere in the application or feature.

ArchitectureModule:

@NgModule({
  imports: [ CommonModule, FormsModule, RouterModule.forChild(routes)],
  declarations: [ TechnologyListComponent, TechnologyDetailComponent, SalesTaxComponent ],
  providers: [ LoggerService, BackendService, TechnologyService ]
})

Alternatively we can register at a component level in the providers property of the @Component metadata:

@Component({
  selector: 'app-sales-tax',
  template: `
      <h2>Sales Tax Calculator</h2>
      Amount: <input #amountBox (change)="0">

      <div *ngIf="amountBox.value">
      The sales tax is
      {{ getTax(amountBox.value) | currency:'EUR':true:'1.2-2' }}
      </div>
  `,
  providers: [ SalesTaxService, TaxRateService ]
})

Registering at a component level means we get a new instance of the service with each new instance of that component.

Key Takeaways:

  • Dependency injection is wired into the Angular framework and used everywhere.
  • The injector is the main mechanism.
  • An injector maintains a container of service instances that it created.
  • An injector can create a new service instance from a provider.
  • A provider is a recipe for creating a service.
  • Register providers with injectors.

We have learned the basics about the eight main building blocks of an Angular application:

  • Modules
  • Components
  • Templates
  • Metadata
  • Data binding
  • Directives
  • Services
  • Dependency Injection

Here is a brief alphabetical list of other important Angular features and services. We will cover most of them in this book.

  • Animations: Animate component behavior without deep knowledge of animation techniques or CSS with Angular's animation library.
  • Change detection: The change detection documentation will cover how Angular decides that a component property value has changed, when to update the screen, and how Angular uses zones to intercept asynchronous activity and run its change detection strategies.
  • Events: The events documentation will cover how to use components and services to raise events with mechanisms for publishing and subscribing to events.
  • Forms: Support complex data entry scenarios with HTML-based validation and dirty-checking.
  • HTTP: Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
  • Lifecycle hooks: Tap into key moments in the lifetime of a component, from its creation to its destruction, by implementing the lifecycle hook interfaces.
  • Pipes: Use pipes in your templates to improve the user experience by transforming values for display. Consider the currency pipe expression:
price | currenncy:'EUR':true  <-- it displays a price of 42.33 as €42.33
  • Router: Navigate from page to page within the client application and never leave the browser.
  • Testing: Run unit tests on our application parts as they interact with the Angular framework using the Angular Testing Platform.

Let's have a look at the running application in our Chromium Browser:

results matching ""

    No results matching ""