Why and how to structure Features in Modules in Angular
This might sound pretty basic, but I encounter these challenges over and over in customer projects and it’s still an ongoing discussion internally.
A central project goal in a recent Angular project was to design features and UI components for reusability. To achieve this, we need to make sure our code is well isolated and has a simple and clear dependency model.
Prologue: Feature vs. Technical Project Structure
When building small apps and looking at common code samples in the internet a lot of devs (including myself) tend to come up with a project structure like this:
The code is structured by technical aspects like ViewModels
, Models
, Views
, Services
etc. And a lot of times we find folders like Helpers
which usually host everything that didn’t fit into one of the standard folders.
When building a larger app, it’s generally a better idea to structure code by features a style that’s also recommended by Angular which might look like this:
This way it’s much easier to find stuff because the structure reflects what the user sees in the app. The code becomes also more resilient to changes as features are isolated from each other and features or UI components can more easily be reused and tested. Also in the case of Angular the code style guidelines suggest putting the type of class we’re creating into the class name like [my-name].component.ts
or [my-name].service.ts
making it useless to additionally group all services into a service folder as it’s already visible in the filename that it’s a service.
Why put a Feature into a Module?
Just grouping services and components into folders by feature doesn’t fully deliver on the above idea. Using a module to define a feature in Angular allows for isolation, portability and lazy loading. So we choose to group features in modules by default (no rule without exceptions…).
Example: Blocking Progress Indicator
A good sample for the mentioned benefits is a blocking progress indicator that overlays the current UI with a progress animation whenever a feature needs to block the UI from interaction e.g. to do an asynchronous operation.
Spinner Module
First, we need to create a module and a component with a simple loading animation. So let’s take one of the awesome pure CSS3 animations from Tobias Ahlin’s SpinKit project and wrap it in a component and in a module.
Blocking Progress Module
Now to our BlockingProgressModule
. This module will import the SpinnerModule
and add additional blocking UI as well as an API to enable/disable the animation.
To allow external components or services to toggle the animation we can use a service with observables. Our UI component then binds to those observables to show/hide the animation and external code can trigger those observables.
So, let’s add a small component to render the SpinnerComponent
and bind to the service.
Use Module in multiple modules
The BlockingProgressModule
is made to be used across multiple components and it’s UI will be hosted in the main app module. We could just import the BlockingProgressModule
into our AppModule
and then go and inject the service into whatever child module we like. But this would mean that we introduce nontransparent dependencies into those child modules as they rely on a service that’s provided by a module that’s not directly imported into the child module by rather been passed on through the dependency tree. Of course this is not a real problem if you’re not planning to reuse the child module anyway.
So let’s import the BlockingProgressModule
into every module that uses it. Now we have a new challenge: As we import the module multiple times (AppModule
+ child modules) multiple instances of our service are being created at runtime making the service pretty much useless.
To solve this issue we can simply use the .forRoot()
method as described here to create a singleton instance of our module’s services. To make this work we need to provide our BlockingProgressService
different using the static forRoot()
method:
Then import the module in our AppModule
using the forRoot()
method:
All child/feature modules will import the BlockingProgressModule
the standard way like so:
Shared/Core
Our BlockingProgressModule
is a good example of a feature that will be shared across multiple feature modules. Another one would be authentication or custom directives. While one could argue that these are features of their own, it might still be handy to group them for usage convenience and a better overview it the app’s root folder into a core module. A good entry into the discussion around shared and core modules is provided here: https://angular.io/guide/ngmodule-faq#what-kinds-of-modules-should-i-have-and-how-should-i-use-them
Conclusion
Hope this explanation provides some context to those who struggle to structure and connect modules in Angular. There might not be a one fits all approach and we’re still discussing aspects of this internally. Please feel free to provide feedback.