This tutorial presents a series of articles on building an effective Angular application architecture, based on best practices used at Ekobit.
In the Angular architecture tutorial I you were introduced with an initial structure of an Angular application, together with a process of building and preparing your first Angular application for deployment.
As an Angular project evolves, in this part of the Angular architecture tutorial we would create modules, load them dynamically, share their data and start to build modular architecture.
Prerequisites
It’s assumed that you’re familiar with the basics of NuGet packages and the Single-Page Application (SPA) paradigm that’s used to build client-side web applications. It’s also assumed that you’re familiar with TypeScript, and that you have at least some experience in building Angular applications.
Expanding your architecture
For most smaller applications and architectures where the separation of concerns isn’t relevant, having just an application in the src folder is sufficient.
As we progress in application complexity by fulfilling our business requirements, an application will evolve to have more distinct parts (i.e. modules). Each part will serve its purpose by bringing its specific functionality to an end-user. This paradigm is supported and well-known not only to Angular application developers but also to those who work with TypeScript – and lately, JavaScript as well. There’s always one root module, which can hold other modules that can also be dynamically loaded.
Later on, more projects can be bound together in the same application folder (and source control repository), as the complexity and demands of the project usually only increase.
Modular project architecture in Angular
For creating a new module inside an existing Angular application, you can use the following command:
ng g module <module-name> --routing=tru
This command will create a new module in the folder that carries the module name, as well as an additional module for routing.
The ‘glue’ that connects the modules to one another is the Angular routing. Whenever a call is made to fetch a component or another asset from a module, the call is firstly found and checked by a routing module.
As seen from the screenshot below, the root module – usually called the ‘app.module‘) includes the root routing module – the app-routing.module – which defines navigational routes for accessing the root modules’s content. Each time a client makes a request, these routes will be parsed from top to bottom to find out the correct match. If the request is valid, the client will be navigated to the corresponding content. As in the example below, the root module can define routes from outside the module, and also evaluate paths that are dynamically loaded.
As mentioned, each route can be validated before the user is navigated, and this is usually done to protect the content with the user authorization,such as OAuth or some other authorization.
Other modules have their own navigation in their corresponding routing modules, and they are initialized with the indication that they correspond to child routes.
There are alternative ways of putting modules together, such as loading modules by Angular’s compiler and using module factories. But for most cases, such alternatives aren’t necessary.
Sharing data between the modules
In order to make the code inside the modules available, a module can be imported into the main (root) module.
In the example above, the devices (‘child’) module is imported into the root module in this way. This is a more direct way to bind modules together because the target module is being compiled along with the main module, and the overall size increases accordingly. This approach can be used when there’s a tight connection between the root and imported modules, as all components and directives will be implicitly available (in the HTML templates, for example).
The most common problem in such application organization is that a child module can’t be imported into some other child module. This is because it’s already been imported into a root module. Because of this, a shared module paradigm can be used.
It’s assumed that a shared module consists of code that’s not tightly coupled with the rest of the application’s structure. In general, it contains directives, pipes, models, services and helper classes used throughout the application, a feature that embodies the DRY (Don’t repeat yourself) principle. Some third-party library providers use this paradigm to categorize their delivered application bundles (also known as ‘barrels‘) by type (components, directives, validators) and functionality set (a dropdown control, a pie chart control, etc).
A shared module should be imported by each module and not by the root module. Use this principle to greatly increase code reuse and to decrease module size. But, because of the , be careful with the application’s increased complexity!
Sharing data between dynamically loaded modules is somewhat harder because there’s no direct communication between the main module and the loaded module.
There are, however, a couple of ways to circumvent this – easiest of them is through route parameterization.
When navigating to a distant (outside) route with a route parameter, the code inside the target component should read the route parameter from the router and act accordingly.
Alternatively, component attributes – as in any other common case – can be set through the @input variable of the target component. Be careful though: communication through the (singleton) service should be avoided, as there’s no guarantee about the service’s availability and the modus operandi intended from within the target component.
____________________________________________________________
Note: All of the source codes used in these articles are available here.