Search icon CANCEL
Subscription
0
Cart icon
Cart
Close icon
You have no products in your basket yet
Save more on your purchases!
Savings automatically calculated. No voucher code required
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Architecting Angular Applications with Redux, RxJS, and NgRx

You're reading from  Architecting Angular Applications with Redux, RxJS, and NgRx

Product type Book
Published in Mar 2018
Publisher Packt
ISBN-13 9781787122406
Pages 364 pages
Edition 1st Edition
Languages
Toc

Table of Contents (12) Chapters close

Preface Quick Look Back at Data Services for Simple Apps 1.21 Gigawatt – Flux Pattern Explained Asynchronous Programming Functional Reactive Programming RxJS Basics Manipulating Streams and Their Values RxJS Advanced Redux NgRx – Reduxing that Angular App NgRx – In Depth Other Books You May Enjoy

Using ES2015 modules

So far, we have mentioned that models are just plain classes. An ES2015 module is just one file. Within that file lives both public and private constructs. Things that are private are only visible within that file. Things that are public can be used outside said file. In Angular, Es2015 modules aren't used only for models but for all imaginable constructs such as components, Directives, Pipes, Services, and so on. This is because ES2015 modules are an answer to how we split our project into smaller parts, which provides us with the following benefits:

  • Many small files makes it easier to parallelize the work you do and have many developers work on it at the same time
  • The ability to hide data by, making some parts of your application public and some other private
  • Code reuse
  • Better maintainability

We have to remember what web development used to look like to understand these statements. When the web was young our JavaScript code more often than not consisted of one file. That quickly became a huge mess. There have been different techniques over the years to find a way to split up our app into many small files. Many small files have made it easier to maintain and also to get a good overview of what is going on, among many other benefits. There have been other issues though. As all these small files had to be stitched back together before being shipped with the app, a process called bundling, we suddenly had one giant file where functions and variables could by mistake affect each other due to naming collisions. A way to attack that problem is to deal with something called information hiding. This to ensure the variables and functions we created are only visible to certain other constructs. There are multiple ways, of course, to address this issue. ES2015 has a private by default way about them. Everything declared in an ES2015 is private by default unless you explicitly export it, thereby making it publicly accessible to other modules that import the aforementioned module.

So how does this connect to the previous statements? Any module system really allows us to maintain visibility in our project as it grows with us. The alternative is one file which is complete chaos. As for several developers working at the same time, any way of logically dividing up the app makes it easier to divide up the workstreams between developers.

Consuming a module

In ES2015, we use the import and from keywords to import one or several constructs like so:

import { SomeConstruct } from './module';

The imported file looks like this:

export let SomeConstruct = 5;

The basic operations involved, working with ES2015 modules, can be summarized as follows:

  • Define a module and write the business logic of the module
  • Export the constructs you want to make public
  • Consume said module with an import keyword from a consumer file

Of course there is a bit more to it than that, so let's look at what else you can do in the next subsection.

An Angular example

We have been using ES2015 imports extensively throughout this chapter already, but let's emphasize when that was. As mentioned, all constructs used ES2015 modules, models, services, components, and modules. For the module, this looked like this:

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

@NgModule({
declarations: [],
imports: [],
exports: [],
providers: []
})
export class FeatureModule {}

Here, we see that we import the functionality we need and we end up exporting this class, thereby making it available for other constructs to consume. It's the same thing with modules, like so:

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

@Component({
selector: 'example'
})
export class ExampleComponent {}

The pipe, directive, and filter all follow the same pattern of importing what they need and exporting themselves to be included as part of an NgModule.

Multiple exports

So far, we have only shown how to export one construct. It is possible to export multiple things from one module by adding an export keyword next to all constructs that you wish to export, like so:

export class Math {
add() {}
subtract() {}
}

export const PI = 3.14

Essentially, for everything you want to make public you need to add an export keyword at the start of it. There is an alternate syntax, where instead of adding an export keyword to every construct, we can instead define within curly brackets what constructs should be exported. It looks like this:

class Math {
add() {}
subtract() {}
}

const PI = 3.14

export {
Math, PI
}

Whether you put export in front of every construct or you place them all in an export {}, then end result is the same, it's just a matter of taste which one to use. To consume constructs from this module, we would type:

import { Math, PI } from './module';

Here, we have the option of specifying what we want to import. In the previous example, we have opted to export both Math and PI, but we could be content with only exporting Math, for example; it is up to us.

The default import/export

So far, we have been very explicit with what we import and what we export. We can, however, create a so-called default export, which looks somewhat different to consume:

export default class Player {
attack() {}
move() {}
}

export const PI = 3.13;

To consume this, we can write the following:

import Player from './module';
import { PI } from './module'

Note especially the first row where we no longer use the curly brackets, {}, to import a specific construct. We just use a name that we make up. In the second row, we have to name it correctly as PI, but in the first row we can choose the name. The player points to what we exported as default, that is, the Player class. As you can see, we can still use the normal curly brackets, {}, to import specific constructs if we want to.

Renaming imports

Sometimes we may get a collision, with constructs being named the same. We could have this happening:

import { productService } from './module1/service'
import { productService } from './module2/service'; // name collision

This is a situation we need to resolve. We can resolve it using the as keyword, like so:

import { productService as m1_productService }
import { productService as m2_productService }

Thanks to the as keyword, the compiler now has no problem differentiating what is what.

The service

We started this main section talking about how ES2015 modules are for all constructs in Angular. This section is about services, and services are no different when it comes to using ES2015 modules. Services we use should be declared in a separate file. If we intend to use a service, we need to import it. It needs to be imported for different reasons though, depending on what type of service it is. Services can be of two types:

  • Services without dependencies
  • Services with dependencies

Service without dependencies

A service without dependencies is a service whose constructor is empty:

export Service {
constructor(){}
getData() {}
}

To use it, you simply type:

import { Service } from './service'
let service = new Service();
service.getData();

Any module that consumes this service will get their own copy of the code, with this kind of code. If you, however, want consumers to share a common instance, you change the service module definition slightly to this:

class Service {
constructor() {}
getData() {}
}
const service = new Service();
export default service;

Here, we export an instance of the service rather than the service declaration.

Service with dependencies

A service with dependencies has dependencies in the constructor that we need help resolving. Without this resolution process, we can't create the service. Such a service may look like this:

export class Service {
constructor(
Logger logger: Logger,
repository:Repository
) {}
}

In this code, our service has two dependencies. Upon constructing a service, we need one Logger instance and one Repository instance. It would be entirely possible for us to find the Logger instance and Repository instance by typing something like this:

import { Service } from './service'
import logger from './logger';
import { Repository } from './repository';

// create the service
let service = new Service( logger, new Repository() )

This is absolutely possible to do. However, the code is a bit tedious to write every time I want a service instance. When you start to have 100s of classes with deep object dependencies, a DI system quickly pays off.

This is one thing a Dependency Injection library helps you with, even if it is not the main motivator behind its existence. The main motivator for a DI system is to create loose coupling between different parts of the system and rely on contracts rather than concrete implementations. Take our example with the service. There are two things a DI can help us with:

  • Switch out one concrete implementation for another
  • Easily test our construct

To show what I mean, let's first assume Logger and Repository are interfaces. Interfaces may be implemented differently by different concrete classes, like so:

import { Service } from './service'
import logger from './logger';
import { Repository } from './repository';

class FileLogger implements Logger {
log(message: string) {
// write to a file
}
}

class ConsoleLogger implements Logger {
log(message: string) {
console.log('message', message);
}
}

// create the service
let service = new Service( new FileLogger(), new Repository() )

This code shows how easy it is to switch out the implementation of Logger by just choosing FileLogger over ConsoleLogger or vice versa. The test case is also made a lot easier if you only rely on dependencies coming from the outside, so that everything can therefore be mocked.

You have been reading a chapter from
Architecting Angular Applications with Redux, RxJS, and NgRx
Published in: Mar 2018 Publisher: Packt ISBN-13: 9781787122406
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime}