14. Layers

14.1. Components Layer

The components layer encapsulates all components presenting the current application view state, which means data to be shown to the user. The term component refers to a component described by the standard Web Components. So this layer has all Angular components, directives and pipes defined for an application. The main challenges are:

  • how to structure the components layer (see File Structure Guide)

  • decompose components into maintainable chunks (see Component Decomposition Guide)

  • handle component interaction

  • manage calls to the services layer

  • apply a maintainable data and eventflow throughout the component tree

14.1.1. Smart and Dumb Components

The architecture applies the concept of Smart and Dumb Components (syn. Containers and Presenters). The concept means that components are devided into Smart and Dumb Components.

A Smart Component typically is a toplevel dialog inside the component tree.

  • a component, that can be routed to

  • a modal dialog

  • a component, which is placed inside AppComponent

A Dumb Component can be used by one to many Smart Components. Inside the component tree a Dumb Component is a child of a Smart Component.

Component Tree
Figure 25. Component tree example

As shown the topmost component is always the AppComponent in Angular applications. The component tree describes the hierarchy of components starting from AppComponent. The figure shows Smart Components in blue and Dumb Components in green. AppComponent is a Smart Component by definition. Inside the template of AppComponent placed components are static components inside the component tree. So they are always displayed. In the example OverviewComponent and DetailsComponent are rendered by Angular compiler depending on current URL the application displays. So OverviewComponents subtree is displayed if the URL is /overview and DetailsComponents subtree is displayed if the URL is /details. To clarify this distinction further the following table shows the main differences.

Table 20. Smart vs Dumb Components
Smart Components Dumb Components

contain the current view state

show data via binding (@Input) and contain no view state

handle events emited by Dumb Components

pass events up the component tree to be handled by Smart Components (@Output)

call the services layer

never call the services layer

use services

do not use services

consists of n Dumb Components

is independent of Smart Components

14.1.2. Interaction of Smart and Dumb Components

With the usage of the Smart and Dumb Components pattern one of the most important part is component interaction. Angular comes with built in support for component interaction with @Input() and @Output() Decorators. The following figure illustrates an unidirectional data flow.

  • Data always goes down the component tree - from a Smart Component down its children.

  • Events bubble up, to be handled by a Smart Component.

Smart and Dumb Components Interaction
Figure 26. Smart and Dumb Component Interaction

As shown a Dumb Components role is to define a signature by declaring Input and Output Bindings.

  • @Input() defines what data is necessary for that component to work

  • @Output() defines which events can be listened on by the parent component

Listing 8. Dumb Components define a signature
export class ValuePickerComponent {

  @Input() columns: string[];
  @Input() items: {}[];
  @Input() selected: {};
  @Input() filter: string;
  @Input() isChunked = false;
  @Input() showInput = true;
  @Input() showDropdownHeader = true;

  @Output() elementSelected = new EventEmitter<{}>();
  @Output() filterChanged = new EventEmitter<string>();
  @Output() loadNextChunk = new EventEmitter();
  @Output() escapeKeyPressed = new EventEmitter();

}

The example shows the Dumb Component ValuePickerComponent. It describes seven input bindings with isChunked, showHeader and showDropdownHeader being non mandatory as they have a default value. Four output bindings are present. Typically, a Dumb Component has very little code to no code inside the TypeScript class.

Listing 9. Smart Components use the Dumb Components signature inside the template
<div>

  <value-input
    ...>
  </value-input>

  <value-picker
    *ngIf="isValuePickerOpen"
    [columns]="columns"
    [items]="filteredItems"
    [isChunked]="isChunked"
    [filter]="filter"
    [selected]="selectedItem"
    [showDropdownHeader]="showDropdownHeader"
    (loadNextChunk)="onLoadNextChunk()"
    (elementSelected)="onElementSelected($event)"
    (filterChanged)="onFilterChanged($event)"
    (escapeKeyPressed)="onEscapePressedInsideChildTable()">
  </value-picker>

</div>

Inside the Smart Components template the events emitted by Dumb Components are handled. It is a good practice to name the handlers with the prefix on* (e.g. onInputChanged()).

14.2. Services Layer

The services layer is more or less what we call 'business logic layer' on the server side. It is the layer where the business logic is placed. The main challenges are:

  • Define application state and an API for the components layer to use it

  • Handle application state transitions

  • Perform backend interaction (XHR, WebSocket, etc.)

  • Handle business logic in a maintainable way

  • Configuration management

All parts of the services layer are described in this chapter. An example which puts the concepts together can be found at the end Interaction of Smart Components through the services layer.

14.2.1. Boundaries

There are two APIs for the components layer to interact with the services layer:

  • A store can be subscribed to for receiving state updates over time

  • A use case service can be called to trigger an action

To illustrate the fact the follwing figure shows an abstract overview.

Smart and Dumb Components Interaction
Figure 27. Boundaries to components layer

14.2.2. Store

A store is a class which defines and handles application state with its transitions over time. Interaction with a store is always synchronous. A basic implementation using rxjs can look like this.

Tip
A more profound implementation taken from a real-life project can be found here (Abstract Class Store).
Listing 10. Store defined using rxjs
@Injectable()
export class ProductSearchStore {

  private stateSource = new BehaviorSubject<ProductSearchState>(defaultProductSearchState);
  state$ = this.stateSource.asObservable();

  setLoading(isLoading: boolean) {
    const currentState = this.stateSource.getValue();
    this.stateSource.next({
      isLoading: isLoading,
      products: currentState.products,
      searchCriteria: currentState.searchCriteria
    });
  }

}

In the example ProductSearchStore handles state of type ProductSearchState. The public API is the property state$ which is an observable of type ProductSearchState. The state can be changed with method calls. So every desired change to the state needs to be modeled with an method. In reactive terminology this would be an Action. The store does not use any services. Subscribing to the state$ observable leads to the subscribers receiving every new state.

This is basically the Observer Pattern:
The store consumer registeres itself to the observable via state$.subscribe() method call. The first parameter of subscribe() is a callback function to be called when the subject changes. This way the consumer - the observer - is registered. When next() is called with a new state inside the store, all callback functions are called with the new value. So every observer is notified of the state change. This equals the Observer Pattern push type.

A store is the API for Smart Components to receive state from the service layer. State transitions are handled automatically with Smart Components registering to the state$ observable.

14.2.3. Use Case Service

A use case service is a service which has methods to perform asynchronous state transitions. In reactive terminology this would be an Action of Actions - a thunk (redux) or an effect (@ngrx).

Use Case Service
Figure 28. Use case services are the main API to trigger state transitions

A use case services method - an action - interacts with adapters, business services and stores. So use case services orchestrate whole use cases. For an example see use case service example.

14.2.4. Adapter

An adapter is used to communicate with the backend. This could be a simple XHR request, a WebSocket connection, etc. An adapter is simple in the way that it does not add anything other than the pure network call. So there is no caching or logging performed here. The following listing shows an example.

For further information on backend interaction see Consuming REST Services

Listing 11. Calling the backend via an adapter
@Injectable()
export class ProducsAdapter {

  private baseUrl = environment.baseUrl;

  constructor(private http: HttpClient) { }

  getAll(): Observable<Product[]> {
    return this.http.get<Product[]>(this.baseUrl + '/products');
  }

}

14.2.5. Interaction of Smart Components through the services layer

The interaction of smart components is a classic problem which has to be solved in every UI technology. It is basically how one dialog tells the other something has changed.

An example is adding an item to the shopping basket. With this action there need to be multiple state updates.

  • The small logo showing how many items are currently inside the basket needs to be updated from 0 to 1

  • The price needs to be recalculated

  • Shipping costs need to be checked

  • Discounts need to be updated

  • Ads need to be updated with related products

  • etc.

Pattern

To handle this interaction in a scalable way we apply the following pattern.

Interaction of Smart Components via services layer
Figure 29. Smart Component interaction

The state of interest is encapsualted inside a store. All Smart Components interested in the state have to subscibe to the store’s API served by the public observable. Thus, with every update to the store the subscribed components receive the new value. The components basically react to state changes. Altering a store can be done directly if the desired change is synchronous. Most actions are of asynchronous nature so the UseCaseService comes into play. Its actions are void methods, which implement a use case, i.e., adding a new item to the basket. It calls asynchronous actions and can perform multiple store updates over time.

To put this pattern into perspective the UseCaseService is a programmatic alternative to redux-thunk or @ngrx/effects. The main motivation here is to use the full power of TypeScript’s --strictNullChecks and to let the learning curve not to become as steep as it would be when learning a new state management framework. This way actions are just void method calls.

Example
Smart component interaction example
Figure 30. Smart Components interaction example

The example shows two Smart Components sharing the FlightSearchState by using the FlightSearchStore. The use case shown is started by an event in the Smart Component FlightSearchComponent. The action loadFlight() is called. This could be submitting a search form. The UseCaseService is FlightSearchService, which handles the use case Load Flights.

UseCaseService example

export class FlightSearchService {

  constructor(
    private flightSearchAdapter: FlightSearchAdapter,
    private store: FlightSearchStore
  ) { }

  loadFlights(criteria: FlightSearchCriteria): void {
    this.store.setLoadingFlights(true);
    this.store.clearFlights();

    this.flightSearchAdapter.getFlights(criteria.departureDate,
        {
          from: criteria.departureAirport,
          to: criteria.destinationAirport
        })
      .finally(() => this.store.setLoadingFlights(false))
      .subscribe((result: FlightTo[]) => this.store.setFlights(result, criteria));
  }

}

First the loading flag is set to true and the current flights are cleared. This leads the Smart Component showing a spinner indicating the loading action. Then the asynchronous XHR is triggert by calling the adapter. After completion the loading flag is set to false causing the loading indication no longer to be shown. If the XHR was successful, the data would be put into the store. If the XHR was not successful, this would be the place to handle a custom error. All general network issues should be handled in a dedicated class, i.e., an interceptor. So for example the basic handling of 404 errors is not done here.

Last updated 2019-12-11 12:58:44 UTC