Line-of-Business (LOB) applications are the bread and butter of the software development world. As defined on Wikipedia, LOB is a general term that refers to a product or a set of related products that serve a particular customer transaction, or business need. LOB apps present an excellent opportunity to demonstrate a variety of features and functionality, without getting into the contorted or specialized scenarios that large enterprise applications usually need to address.
The Pareto principle, also known as the 80-20 rule, states that we can accomplish 80% of our goals with 20% of the overall effort. We will be applying the 80-20 rule to the design and architecture of our LOB app. Given the common use cases LOB apps cover, they are, in a sense, perfect for the 80-20 learning experience. With only 20% of the effort, you can learn about 80% of the things you will need to deliver high-quality experiences to your users.
LOB apps have a curious property to them. If you end up building a semi-useful app, the demand for it grows, uncontrollably, and you quickly become the victim of your success. It's challenging to balance the architectural needs of a project; you want to avoid potentially devastating under-engineering and, on the flip side, also avoid costly over-engineering for an app that will never need it.
In this chapter, I'm going to introduce you to router-first architecture, the 80-20 design solution to address the challenges of delivering a modern web application in an incremental and iterative manner.
As you read in Chapter 1, Introduction to Angular and Its Concepts, software architecture doesn't stay static. It's essential to experiment with new ideas by using coding-katas, proofs-of-concept apps, and reference projects, to get better at creating more flexible architectures.
In this and the remaining chapters of the book, we'll set up a new application with rich features that can meet the demands of an LOB application with a scalable architecture and engineering best practices that will help you start small and be able to grow your solution quickly if there's demand. We will follow the Router-first design pattern, relying on reusable components to create a grocery store LOB named LemonMart. We'll discuss the idea of designing around major data entities, and the importance of completing high-level mock-ups for your application before you start to implement various conditional navigation elements, which may change significantly during the design phase.
In this chapter, you will learn to do the following:
The most up-to-date versions of the sample code for the book are on GitHub at the following linked repository. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking for the end-of-chapter snapshot of code under the projects
folder.
For Chapter 7:
npm install
on the root folder to install dependenciesprojects/ch7
npx ng serve ch7
npx ng test ch7 --watch=false
npx ng e2e ch7
npx ng build ch7 --prod
Note that the dist/ch7
folder at the root of the repository will contain the compiled result.
Beware that the source code in the book or on GitHub may not always match the code generated by the Angular CLI. There may also be slight differences in implementation between the code in the book and what's on GitHub, because the ecosystem is ever-evolving. It is natural for the sample code to change over time.
Also, on GitHub, expect to find corrections, fixes to support newer versions of libraries, or side-by-side implementations of multiple techniques for you to observe. You are only expected to implement the ideal solution recommended in the book. If you find errors or have questions, please create an issue or submit a pull request on GitHub for the benefit of all readers.
You may read more about updating Angular in Appendix C, Keeping Angular and Tools Evergreen. You can find this appendix online from https://static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf or at https://expertlysimple.io/stay-evergreen.
Let's start by covering the philosophy behind the design and architecture of our apps.
Whether we develop apps at home, for passion projects, or at the office, for work, we must remain mindful of our purpose: to deliver value. If we don't deliver value with our passion projects, then we won't feel fulfilled or happy. If we fail to deliver value at work, we may not get paid.
Delivering a modern web application is difficult. There are numerous challenges that we need to overcome to be successful:
If you've ever led a project or tried to implement and deliver a project on your own, you'll have realized that there's just never enough time and resources to cover the wide variety of stakeholder, team, and technical needs on any given project. Remember that the Pareto principle, also known as the 80-20 rule, implies that we can accomplish 80% of our goals with 20% of the overall effort.
If we apply the 80-20 rule to our work, we can maximize our output, quality, and happiness. Line-of-business applications are the bread and butter of our industry. Applying the 80-20 rule, we can surmise that most of us are likely to earn most of our income by delivering such applications. Therefore, we should keep our engineering overhead to a minimum, and reduce the delivery risk of our project. By limiting experimentation in production code, we create a predictable environment for our team members, and only introduce changes that we had a chance to vet in proof-of-concept or small apps.
Our 80-20 strategy, combined with discipline, can help us deliver the same project in the same time with more features and better quality. By treating our careers as marathons and not a series of sprints, you can find yourself in a position of delivering high-quality solutions, project after project, without feeling burned out.
Line-of-business applications are, according to Wikipedia, a "set of critical computer applications perceived as vital to running an enterprise." LOB apps are what most developers end up developing, even though we may think we develop small apps or large enterprise apps. Consider the following illustration, which demonstrates the kinds of apps we might develop, placed on an axis relative to their size and scope:
Figure 7.1: Relative size and scope of four kinds of apps
From my perspective, we think about four kinds of apps when we begin developing software:
Billion user scale apps are completely niche implementations that rarely have needs that align with the vast majority of apps that are out there. For this reason, we must classify these apps as outliers.
Small apps start small. Architecturally, they're likely to be initially under-engineered. As you add features and team members to work on a small app, at some point, you're going to run into trouble. As your team size and feature set grow, or the overall complexity of the app increases, the architectural needs of the application grow exponentially.
Once you cross the inflection point of the amount of complexity your architecture can bear, you're left with a costly reengineering effort to get back on track. See the following graph, illustrating this idea:
Figure 7.2: Architectural journey of a small app
The area under the feature line represents under-engineering, which introduces risk to your project. The area above the feature line shows the required engineering overhead to support the features needed. In comparison, large enterprise apps start with a massive over-engineering effort, as shown in the following diagram:
Figure 7.3: Architectural journey of a large enterprise app
As time goes on and the overall complexity of the system increases, large enterprise apps can also face a similar inflection point, where the original architecture can become inadequate. With careful planning and management, you can avoid trouble and protect the significant initial investment made. Such large enterprise apps require hundreds of developers, with multiple levels of managers and architects, to execute successfully. Similar to billion-user scale apps, these apps can also have niche architectural needs. In between the small apps and the large enterprise apps that we develop lie LOB apps.
Figure 7.4: Dynamic nature of software evolution
As shown in the preceding diagram, small apps can grow and morph into LOB apps, and large enterprise apps can become under-utilized as users ignore the features that they never need, but keep the app to serve a singular purpose as a LOB app. In either case, despite our best efforts, we ultimately end up delivering an inefficient solution for the problem we're solving. None of us have a crystal ball to see the future, and planning and engineering can only do so much for us in an unpredictable business setting; we need to rely on the 80-20 rule to come up with an architecture that is flexible to change, but adequate to meet most business requirements.
Router-first architecture aims to maintain optimal architectural overhead, so that in the rush to deliver all required features, costly re-engineering or late-stage crunch can be avoided. Let's see how.
We covered the what of software development, but we must also consider the why, when, where, and who, before we can get to the how. When we develop apps for learning or passion projects, we usually end up under-engineering our projects. If your passion project somehow becomes an overnight success, then it becomes costly to maintain or keep adding features to your app. In this case, you're likely to face a choice to either bear the cost of ongoing maintenance, or rewrite your application.
When we develop apps for work, we tend to be more conservative, and we're likely to over-engineer our solution. However, if you only code for work, then you're likely to experiment in production-bound code. It is dangerous to experiment in a codebase with other team members. You may be introducing a new pattern, without your team understanding the consequences of your choices. You're also less likely to be aware of mid-to long-term risks or benefits of the technologies you are introducing.
Reckless experimentation can also have a severe negative impact on your team members. In a team of senior and experienced software engineers, you can likely get away with experimenting in a moving car. However, we are likely to have team members of varying backgrounds and learning styles on our teams. Some of us have computer science degrees, some of us are lone wolves, and some of us depend a bit too much on Stack Overflow. Some of us work at companies that are great at supporting professional growth, but some of us work at places that won't even give you a day to learn something new. So, when we are experimenting, we must consider our environment; otherwise we can cause our colleagues to work overtime or feel helpless and frustrated.
With a disciplined and balanced approach, we can reduce the number of bugs delivered, avoid costly rework, and work with a group of people who are all moving in the same direction. We also need the right architecture, tools, and patterns/practices to deliver successfully. In summary, our approach must consider:
Ideally, we need to maintain optimal engineering overhead. Our architecture should support our short-term needs while being extensible, so we can pivot in different directions if our mid-or long-term needs change without having to rewrite large swaths of code. Consider the following diagram, in contrast to the ones about small and large enterprise apps in the previous section:
Figure 7.5: Ideal architectural journey of a LOB app
Router-first architecture aims to help you find the balance in the engineering overhead, feature delivery, and flexibility of your codebase. However, you must bring the discipline yourself.
or Shu Ha Ri is a concept that can help bring discipline to your work. It is a way of thinking that instructs you first to master the basics without worrying about the underlying theory, then master the theory, and finally be able to adapt what you mastered to your needs. However, if you skip steps 1 or 2, you are going to find yourself adapting the wrong thing in the wrong way.
Having covered the what, why, when, where, and who, let's jump into the how in the next section.
Router-first architecture is a way to:
There are seven steps to implementing router-first architecture:
Each step will be covered in more detail in this and coming chapters, as noted previously. Before we go over these steps at a high level, let's first cover feature modules in Angular, which are an important fundamental technical concept.
In Chapter 1, Introduction to Angular and Its Concepts, we covered Angular's architecture at a high level and introduced the concepts of lazy loading and routing. Feature modules are a key component in implementing lazy loading. There are two kinds of modules, the root module and feature modules. Modules are implemented by the class NgModule
. An NgModule
contains all the necessary metadata to render components and inject services. A component without a module doesn't do much.
An Angular application is defined by an NgModule
that sits at the root of the application. This is called the root module. The root module is responsible for rendering what appears in the <app-root>
element in your index.html
file. Locate the root module in the following diagram:
Figure 7.6: Major architectural components of Angular
An NgModule can contain many other NgModules. An Angular app only has one root module, so by definition every other NgModule becomes a feature module. In the preceding diagram, you can see that you can organize a group of components (Cmp) and services (Svc) into feature modules. Grouping functionality into modules allows us to organize our code into chunks, which can be separated from the initial payload of our application.
This idea of root and feature modules represents a parent/child relationship, which is a concept that extends to other functionality and frameworks. For example, note that the preceding diagram injects a root router into the root module. A root router can have child routes. Child routes can be configured to load feature modules. Similarly, NgRx has root and feature module-level stores to organize the state data of your application.
For all intents and purposes, any mention of a sub-module, child module, or a feature module in this book refers to the same thing: a module that is not the root module.
Feature modules and child routes allows for a separation of concerns between major components of your application. Two teams can work on two different modules without interfering with each other. This separation means that any dependency required by a feature module must be explicitly added to the imports, declarations, or providers of that module. This can seem repetitive and annoying, when sharing code between modules, but it is a necessary evil.
In Angular, by default, services are singletons – one instance per module. Before importing a service that's already imported to the root module into a feature module, consider if this is truly the desired behavior. A service that is provided in the root module is available to be imported in a feature module without needing to be provided again. Providing a service in the root and feature modules will result in having multiple instances of that service in memory, which breaks your expectation that, by default, services are singletons. In Chapter 8, Designing Authentication and Authorization, you will see this in action when we implement the AuthService
.
With the introduction of the Ivy rendering engine in Angular 9, the road is paved to create self-describing components. Self-describing components do not need an NgModule to be useful. With future versions of Angular it will be possible to implement simple apps without the whole ceremony (read: boilerplate code) of modules.
Now, let's go over the seven steps of router-first architecture at a high level.
Developing a roadmap and establishing the scope of your project early on is critical to getting the high-level architecture right. Creating a backlog, wireframes, mock-ups and interactive prototypes will help you define the map before getting on the road and capture the vision concretely. It is important to remember to bring tools only when necessary. Don't start with Photoshop, when a piece of paper and a pencil will do. If stakeholders and team members understand what is being developed, then it will be possible to deliver your solution iteratively and incrementally. However, don't fall into the perfection trap. Save the tweaking and furniture rearranging for after the fundamentals are in place and agreed upon.
Document every artifact you create. Later in the chapter we cover how you can leverage GitHub Wikis to store your artifacts.
Later in this chapter, we will go over how to develop a roadmap and a technique to define your scope, building on the roadmap building techniques covered in Chapter 3, Creating a Basic Angular App.
First-paint matters, a lot! According to Google Analytics data gathered by the Angular Team in 2018, 53% of mobile users abandoned a website when load times exceeded 3 seconds. During the same time period most websites were consumed on mobile devices, around 70%+ in the US and 90%+ in China. As we covered in Chapter 5, Delivering High-Quality UX with Material, UI libraries and static assets can add significant size to your application. Given that most content is consumed on mobile, it's very important to defer the loading of non-critical assets.
We defer loading of assets by divvying up the parts of our Angular application into feature modules. This way Angular can load only the assets that are necessary to render the current screen and dynamically download further resources as they are needed. A good way to divide your application into feature modules is by defining the various user roles your application may use. User roles normally indicate the job function of a user, such as a manager or data-entry specialist. In technical terms, they can be thought of as a group of actions that a particular class of user is allowed to execute. After all, a data-entry specialist won't ever see most of the screens that a manager can, so why deliver those assets to those users and slow down their experience?
Lazy loading is critical in creating a scalable application architecture, allowing you to deliver high-quality and efficient products. Lazy loading is a low-hanging fruit that we will tackle as a baseline design goal. It can be costly to implement lazy loading after the fact.
Starting with Angular 9, it is possible to lazy load individual components. Angular 9's Ivy rendering engine enables self-describing and standalone components. Components that do not require all the bootstrapping that an Angular application requires have the potential to revolutionize and simplify how we design applications. However, it is not yet feasible to design apps this way. Expect future versions of Angular to introduce public APIs that make it easy to use the new features, reducing the need to carefully design feature modules early on.
Later in this chapter, you will learn about how to implement lazy loading using feature modules.
Configuring lazy loading can be tricky, which is why it is essential to nail down a walking-skeleton navigation experience early on. Implementing a clickable version of your app will help you gather feedback from users early on. That way, you'll be able to work out fundamental workflow and integration issues quickly. Additionally, you'll be able to establish a concrete representation of the scope of your current development effort. Developers and stakeholders alike will be able to better visualize how the end product will look.
A walking-skeleton also sets the stage for multiple teams to work in tandem. Multiple people can start developing different feature modules or components at the same time, without worrying about how the puzzle pieces are going to come together later on. By the end of this chapter, you will have completed implementing the walking-skeleton of the sample app LemonMart.
As highlighted in Chapter 10, RESTful APIs and Full-Stack Implementation, stateless design in full-stack architecture is critical to implementing a maintainable application. As covered in Chapter 1, Introduction to Angular and Its Concepts, and later in Chapter 12, Recipes – Master/Detail, Data Tables, and NgRx, the flux pattern and NgRx make it possible to achieve an immutable state for your application. However, the flux pattern is likely to be overkill for most applications. NgRx itself leverages a lot of the core technologies present in RxJS.
We are going to use RxJS and the reactive programming paradigm to implement a minimal, stateless, and data-driven pattern for our application. Identifying major data entities, such as invoices or people, that your users will work with is going to help you avoid over-engineering your application. Designing around major data entities will inform API design early on, and help define BehaviorSubject
data anchors that you will use to achieve a stateless, data-driven design. That design will, in turn, ensure a decoupled component architecture, as detailed in Chapter 6, Forms, Observables, and Subjects.
By defining observable data anchors, you can ensure that data across various components will be kept in sync. By writing functional reactive code, leveraging RxJS features, and not storing state in components, we can implement immutable data streams.
We will cover how to design the data models for your application in Chapter 10, RESTful APIs and Full-Stack Implementation, and will continue using these models in the following chapters.
As we discussed in Chapter 1, Introduction to Angular and Its Concepts, decoupling components of your architecture is critical in ensuring a maintainable codebase. In Angular, you can decouple components by leveraging @Input
and @Output
bindings and Router orchestration.
Bindings will help you maintain a simple hierarchy of components, and avoid using dynamic templates in situations where static designs are more effective, such as the creation of multi-page forms.
Router outlets and auxiliary paths allow you to compose your view using the router. Resolvers can help load data by consuming router parameters. Auth guards can help control access to various modules and components. Using router links, you can dynamically customize elements that a user will see in an immutable and predictable way, similar to the way we designed and developed data anchors in the previous step.
If you ensure every component is responsible for loading its own data, then you can compose components via URLs. However, overusing the router can in itself become an anti-pattern. If a parent component logically owns a child component, then the effort to decouple them will be wasted.
In Chapter 6, Forms, Observables, and Subjects, you learned how to enable component interactions using BehaviorSubject
. In Chapter 11, Recipes – Reusability, Routing, and Caching, you will learn how to implement @Input
and @Output
bindings and in the upcoming chapters you will learn about how to implement router features.
Another important idea is differentiating user controls from components. A user control is like a custom date input, or a custom star rater. It is often highly interactive and dynamic code that ends up being highly coupled, convoluted, and complicated code. Such controls may use Angular features no one has ever heard of before, which are most likely not covered in this book.
A component is more like a form with fields, which may contain simple date inputs or a star rater. Because forms encapsulate business functionality, their code must be easy to read and understand. Your code should stick to Angular basics, so the code is stable and easy to maintain, like most of the code that is presented in this book.
By differentiating between user controls and components you can make better decisions when deciding what kind of code you want to make reusable. Creating reusable code is costly. If you create the right reusable code, you can save time and resources. If you create the wrong reusable code, then you can waste a lot of time and resources.
Wire-framing allows you to identify reusable elements early on. User controls will help keep user interaction code separate from business logic. Well-crafted component reuse will enable you to encapsulate domain-specific behavior, and share it later.
It's important to identify self-contained user controls that encapsulate unique behaviors that you wish to create for your app. User controls will likely be created as directives or components that have data-binding properties and tightly coupled controller logic and templates.
Components, on the other hand, leverage router life cycle events to parse parameters and perform CRUD operations on data. Identifying these component reuses early on will result in creating more flexible components that can be reused in multiple contexts (as orchestrated by the router), maximizing code reuse.
We will cover how to create reusable components and user controls in Chapter 11, Recipes – Reusability, Routing, and Caching.
It's essential to remember the underlying features of the language you work with before you consider the features offered by Angular, RxJS, and all the libraries you use. There are decades of software engineering fundamentals that you can leverage to write readable and maintainable code.
First and foremost is the DRY principle. It stands for don't repeat yourself. So, don't copy-paste code. Don't just change a variable or two. Proactively refactor your code to make your functions stateless and reusable. In a few words: don't repeat yourself, don't repeat yourself, and don't repeat yourself.
Leverage object-oriented design. Move behavior to classes; if your person has a fullName
property, don't re-implement the same logic in a dozen different places, but implement it once in the person
class. This means you will need to become familiar with hydration, also known as injecting a JSON object into a newly instantiated class, and serialization using toJSON
. It is important not to abuse OOP. You should still remain stateless, and functional, by avoiding storing state in class parameters.
You can truly unleash the power of OO design by leveraging generics, inheritance, and abstract classes.
TypeScript introduces the concept of interfaces to JavaScript. Interfaces are a concept mostly reserved for statically typed languages. An interface represents an abstract notion of what an object can do, without specifying any implementation details. Furthermore, an interface can be used to document the shape of data. For example, you can write a partial interface of a third-party API to document the fields you're interested in consuming. When other developers read your code, they have an inherent understanding of the structure of the data they're consuming, without having to read documentation on another website.
Interfaces also allow you to morph the shape of your data in a well-defined manner. So, you can write a transform function to transform the shape of external data into internal data. TypeScript will catch any errors you may make. Taking this concept further, you can also use interfaces to flatten data. If the data you receive has a multi-entity relational structure, you can flatten the relationship to decouple the design of the data from your UI code.
Don't overly flatten your data. Arrays and simple shapes for common objects are okay, such as a name object or commonly used domain-specific object.
You should also avoid string literals in your code. Writing business logic where you compare 'apples' !== 'Oranges'
results in unmaintainable code. You should leverage enums
in TypeScript, so your code isn't subject to the spelling mistakes of coders or changing business requirements. So 'oranges' === Fruit.Organes
.
Beyond TypeScript and ECMAScript, Angular also offers helpful functions for you to reuse logic. Angular validators, pipes, route resolvers, and route guards all allow you to share code across components and templates.
The following chapters will demonstrate the aforementioned concepts:
Next, let's start by creating, LemonMart™, a fully featured line-of-business app that you can use as a template to kickstart your next professional project. LemonMart is a robust and realistic project that can support feature growth and different backend implementations, and it comes with a complete and configurable authentication and authorization solution out of the box.
Since its introduction, LemonMart has served more than 160,000 lemons to over 14,000 developers. Zesty!
You can always clone the finished project from GitHub, https://www.github.com/duluca/lemon-mart, whenever you need it. Let's jump right into it.
LemonMart will be a mid-sized line-of-business application with over 90 code files. We will start our journey by creating a new Angular app, with routing and Angular Material configured from the get-go.
It is presumed that you have installed all the requisite software mentioned in Chapter 2, Setting Up Your Development Environment. If you have not, execute the following commands for your OS to configure your environment.
On Windows PowerShell, execute:
PS> Install-Script -Name setup-windows-dev-env
PS> setup-windows-dev-env.ps1
On macOS Terminal, execute:
$> bash <(wget -O - https://git.io/JvHi1)
For more information refer to https://github.com/duluca/web-dev-environment-setup.
With the router-first approach, we want to enable routing early on in our application:
Ensure that @angular/cli
is not installed globally, or you may run into errors:
$ npx @angular/cli new lemon-mart --routing --strict
(Select CSS as the stylesheet format )
Starting with Angular 9, you may use --strict
to turn on TypeScript features like noImplicitAny
, noImplicitReturns
, noFallthroughCasesInSwitch
, and strictNullChecks
. These options will decrease the chances of making coding mistakes, but result in more verbose code. In my opinion, that is a good thing, and this option is highly recommended for production-bound applications.
AppRoutingModule
file has been created for us:
src/app/app-routing.modules.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
We will be defining routes inside the routes array. Note that the routes array is passed in to be configured as the root routes for the application; the default root route is /
.
When configuring your RouterModule
, you can pass in additional options to customize the default behavior of the Router, such as when you attempt to load a route that you're already on. Normally, if the route you're attempting to navigate to is the same as the current one, the router wouldn't take any action. However, if you wanted the router to refresh the page, you would customize the default behavior of the router, such as with RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload' })
. With this setting in place, if you navigate to the same URL that you are on, you will force a reload of the current component.
AppRoutingModule
is registered with AppModule
, as shown:
src/app/app.module.ts
...
import { AppRoutingModule } from './app-routing.module';
@NgModule({
...
imports: [ AppRoutingModule, ... ],
...
})
To quickly apply configuration steps covered in Chapters 2-6 run the following commands:
The following scripts do not require you to use VS Code. If you wish to use another IDE like WebStorm, the npm
scripts that are configured will run equally well.
npm i -g mrm-task-angular-vscode
npx mrm angular-vscode
npm i -g mrm-task-npm-docker
npx mrm npm-docker
build:prod
"scripts": {
...,
"build:prod": "ng build --prod",
}
These settings are continually tweaked to adapt to the ever-evolving landscape of extensions, plug-ins, Angular, and VS Code. Always make sure to install a fresh version of the task by re-running the install
command to get the latest version. Alternatively, you can use the Angular Evergreen extension for VS Code, to run the configuration commands with one click.
Note that if the preceding configuration scripts fail to execute, then the following npm scripts will also fail. In this case, you have two options: revert your changes and ignore these scripts, or manually implement these scripts as covered in earlier chapters (or as demonstrated on GitHub).
npm run style:fix
npm run lint:fix
npm start
and ensure you're running on http://localhost:5000
, instead of the default port 4200
Refer to Chapter 2, Setting Up Your Development Environment, for further configuration details.
You may optionally setup npm Scripts for AWS ECS, which is used in Chapter 13, Highly Available Cloud Infrastructure on AWS, by using mrm-task-npm-aws
.
For more information on the mrm tasks refer to:
We will also need to set up Angular Material and configure a theme to use, as covered in Chapter 5, Delivering High-Quality UX with Material:
$ npx ng add @angular/material
(select Custom, No to global typography, Yes to browser animations)
$ npm i @angular/flex-layout
$ npx ng g m material --flat -m app
material.module.ts
, define a const
modules
array and export MatButtonModule
, MatToolbarModule
, and MatIconModule
, removing CommonModule
app.modules.ts
, import FlexLayoutModule
so Angular Flex Layout can be activatedstyles.css
as shown in the following code:
src/styles.css
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.margin-top {
margin-top: 16px;
}
.horizontal-padding {
margin-left: 16px;
margin-right: 16px;
}
.flex-spacer {
flex: 1 1 auto;
}
index.html
Refer to Chapter 5, Delivering High-Quality UX with Material, for further configuration details.
We will apply custom branding to the app later in this chapter. Next, let's start designing our line-of-business application.
It is important to build a rudimentary roadmap to follow, from the database to the frontend, while also avoiding over-engineering. This initial design phase is critical to the long-term health and success of your project, where any existing silos between teams must be broken down and an overall technical vision well understood by all members of the team. This is easier said than done, and there are volumes of books written on the topic.
In engineering, there's no one right answer to a problem, so it is important to remember that no one person can ever have all the answers, nor a crystal-clear vision. It is important that technical and non-technical leaders create a safe space with opportunities for open discussion and experimentation as part of the culture. The humility and empathy that comes along with being able to court such uncertainty as a team is as important as any single team member's technical capability. Every team member must be comfortable with checking their egos at the door because our collective goal will be to grow and evolve an application to ever-changing requirements during the development cycle. You will know that you have succeeded if individual parts of the software you created are easily replaceable by anyone.
So, let's start by developing a roadmap and identifying the scope of our application. For this, we will be defining user roles and then building a site map, to create a vision of how our app might work.
The first step of our design will be to think about who is using the application and why.
We envision four user states, or roles, for LemonMart:
With this in mind, we can start to create a high-level design for our app.
Develop a high-level site map of your application, as shown:
Figure 7.7: Landing pages for users
I used MockFlow.com's SiteMap tool to create the site map shown: https://sitemap.mockflow.com.
Upon first examination, three high-level modules emerge as lazy-loading candidates:
The Cashier will only have access to the POS module and component. The Clerk will only have access to the Inventory module, which will include additional screens for the Stock Entry, Products, and Categories management components:
Figure 7.8: Inventory pages
Finally, the Manager will be able to access all three modules with the Manager module, including user management and receipt lookup components:
Figure 7.9: Manager pages
There'll be great benefits from enabling lazy loading for all three modules; since Cashiers and Clerks will never use components belonging to other user roles, there's no reason to send those bytes down to their devices. This means as the Manager module gains more advanced reporting features, or new roles are added to the application, the POS module will be unaffected by the bandwidth and memory impact of an otherwise growing application.
This means fewer support calls, and consistent performance on the same hardware for a much longer period of time.
Now that we have our high-level components defined as Manager, Inventory, and POS, we can define them as modules. These modules will be different from the ones you've created so far, for routing and Angular Material. We can create the user profile as a component on the app module; however, note that user profile will only ever be used for already-authenticated users, so it makes sense to define a fourth module only meant for authenticated users in general. This way, you will ensure that your app's first payload remains as minimal as possible. In addition, we will create a Home component to contain the landing experience for our app so that we can keep implementation details out of app.component
:
manager
, inventory
, pos
, and user
modules, specifying their target module and routing capabilities:
$ npx ng g m manager -m app --routing
$ npx ng g m inventory -m app --routing
$ npx ng g m pos -m app --routing
$ npx ng g m user -m app --routing
As discussed in Chapter 2, Setting Up Your Development Environment, if you have configured npx
to automatically recognize ng
as a command, you can save some more keystrokes because you won't have to append npx
to your commands every time. Do not globally install @angular/cli
.
Note the abbreviated command structure, where ng generate module manager
becomes ng g m manager
, and similarly, --module
becomes -m
.
Note that using npx
on Windows may throw an error such as Path must be a string. Received undefined
. This error doesn't seem to have any effect on the successful operation of the command, which is why it is critical to always inspect what the CLI tool generated.
/src/app
│ app-routing.module.ts
│ app.component.css
│ app.component.html
│ app.component.spec.ts
│ app.component.ts
│ app.module.ts
│ material.module.ts
├───inventory
│ inventory-routing.module.ts
│ inventory.module.ts
├───manager
│ manager-routing.module.ts
│ manager.module.ts
├───pos
│ pos-routing.module.ts
│ pos.module.ts
└───user
│ user-routing.module.ts
│ user.module.ts
ManagerModule
has been wired.A feature module implements an @NgModule
similar to app.module
. The biggest difference is that a feature module does not implement the bootstrap
property, which is required for your root module to initialize your Angular app:
src/app/manager/manager.module.ts
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ManagerRoutingModule } from './manager-routing.module'
@NgModule({
imports: [CommonModule, ManagerRoutingModule],
declarations: [],
})
export class ManagerModule {}
Since we have specified the -m
option, the module has been imported into app.module
:
src/app/app.module.ts
...
import { ManagerModule } from './manager/manager.module'
...
@NgModule({
...
imports: [..., ManagerModule],
...
})
In addition, because we also specified the --routing
option, a routing module has been created and imported into ManagerModule
:
src/app/manager/manager-routing.module.ts
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
const routes: Routes = []
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ManagerRoutingModule {}
Note that RouterModule
is being configured using forChild
, as opposed to forRoot
, which was the case for the AppRouting
module. This way, the router understands the proper relationship between routes defined in different modules' contexts and can correctly prepend /manager
to all child routes in this example.
The CLI doesn't respect your tslint.json
settings. If you have correctly configured your VS Code environment with Prettier, your code styling preferences will be applied as you work on each file or globally when you run the prettier
command.
Be sure to run your style:fix
and lint:fix
commands before moving on. Now, let's design how the landing page for LemonMart will look and work.
Consider the following mock-up as the landing experience for LemonMart:
Figure 7.10: LemonMart landing experience
Unlike the LocalCastWeather
app, we don't want all this markup to be in the App
component. The App
component is the root element of your entire application; therefore, it should only contain elements that will persistently appear throughout your application. In the following annotated mock-up, the toolbar marked as 1 will be persistent throughout the app.
The area marked as 2 will house the home
component, which itself will contain a login user control, marked as 3:
Figure 7.11: LemonMart layout structure
It's best practice to create your default or landing component as a separate element in Angular. This helps reduce the amount of code that must be loaded and logic executed in every page, but it also results in a more flexible architecture when utilizing the router.
Generate the home
component with inline template and styles:
$ npx ng g c home -m app --inline-template --inline-style
Note that a component with an inline template and a style is also referred to as a Single File Component or an SFC.
Now, you are ready to configure the router.
Let's get started with setting up a simple route for LemonMart. We need to set up the /
route (also known as the empty route) and the /home
routes to display the HomeComponent
. We also need a wildcard route to capture all undefined routes and display a PageNotFoundComponent
, which also needs to be created:
home
route:
src/app/app-routing.module.ts
...
import { HomeComponent } from './home/home.component'
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
]
...
We first define a path for 'home'
and inform the router to render HomeComponent
by setting the component
property. Then, we set the default path of the application ''
to be redirected to '/home'
. By setting the pathMatch
property, we always ensure that this very specific instance of the home
route will be rendered as the landing experience.
pageNotFound
component with an inline templatePageNotFoundComponent
:
src/app/app-routing.module.ts
import {
PageNotFoundComponent
} from './page-not-found/page-not-found.component'
...
const routes: Routes = [
...
{ path: '**', component: PageNotFoundComponent },
]
...
This way, any route that is not matched will be directed to the PageNotFoundComponent
.
When a user lands on the PageNotFoundComponent
, we would like them to be redirected to the HomeComponent
using the routerLink
direction:
PageNotFoundComponent
, replace the inline template to link back to home
using routerLink
:
src/app/page-not-found/page-not-found.component.ts
...
template: `
<p>
This page doesn't exist. Go back to
<a routerLink="/home">home</a>.
</p>
`,
...
This navigation can also be done via an <a href>
tag implementation; however, in more dynamic and complicated navigation scenarios, you will lose features such as automatic active link tracking or dynamic link generation.
The Angular bootstrap process will ensure that AppComponent
is inside the <app-root>
element in your index.html
. However, we must manually define where we would like HomeComponent
to render, to finalize the router configuration.
AppComponent
is considered a root element for the root router defined in app-routing.module
, which allows us to define outlets within this root element to dynamically load any content we wish using the <router-outlet>
element:
AppComponent
to use inline template and styles, deleting any existing content in the html and css files<router-outlet>
for the content to render:
src/app/app.component.ts
...
template: `
<mat-toolbar color="primary">
<a mat-button routerLink="/home"><h1>LemonMart</h1></a>
</mat-toolbar>
<router-outlet></router-outlet>
`,
Now, the contents of home
will render inside <router-outlet>
.
In order to construct an attractive and intuitive toolbar, we must introduce some iconography and branding to the app so that the users can easily navigate through the app with the help of familiar icons.
In terms of branding, you should ensure that your web app has a custom color palette and integrates with desktop and mobile browser features to bring forward your app's name and iconography.
Pick a color palette using the Material Color tool, as discussed in Chapter 5, Delivering High-Quality UX with Material. Here's the one I picked for LemonMart:
custom-theme.scss
to lemonmart-theme.scss
angular.json
with the new theme file name
angular.json
"apps": [ {
...
"styles": [
"src/lemonmart-theme.scss",
"src/styles.css"
],
...
}]
You can also grab LemonMart-related assets from GitHub at https://github.com/duluca/lemon-mart.
For the Local Weather app, we replaced the favicon.ico
file to brand our app in the browser. While this would've been enough ten years ago, today's devices vary wildly, and each platform can leverage optimized assets to better represent your web app within their operating systems. Next, let's implement a more robust favicon.
You need to ensure that the browser shows the correct title text and icon in a Browser tab. Further, a manifest file should be created that implements specific icons for various mobile operating systems, so that if a user pins your website, a desirable icon is displayed similar to other app icons on a phone. This will ensure that if a user favorites or pins your web app on their mobile device's home screen, they'll get a native-looking app icon:
Figure 7.12: LemonMart's signature logo
When using images you find on the internet, pay attention to applicable copyrights. In this case, I have purchased a license to be able to publish this lemon logo, but you may grab your own copy at the following URL, given that you provide the required attribution to the author of the image: https://www.flaticon.com/free-icon/lemon_605070.
favicon.ico
and manifest files using a tool such as https://realfavicongenerator.netfavicons.zip
file into your src
folderangular.json
file to include the new assets in your app:
angular.json
"apps": [
{
...
"assets": [
"src/assets",
"src/favicon.ico",
"src/android-chrome-192x192.png",
"src/favicon-16x16.png",
"src/mstile-310x150.png",
"src/android-chrome-512x512.png",
"src/favicon-32x32.png",
"src/mstile-310x310.png",
"src/apple-touch-icon.png",
"src/manifest.json",
"src/mstile-70x70.png",
"src/browserconfig.xml",
"src/mstile-144x144.png",
"src/safari-pinned-tab.svg",
"src/mstile-150x150.png"
]
<head>
section of your index.html
:
src/index.html
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch- icon.png?v=rMlKOnvxlK">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=rMlKOnvxlK">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=rMlKOnvxlK">
<link rel="manifest" href="/manifest.json?v=rMlKOnvxlK">
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=rMlKOnvxlK" color="#b3ad2d">
<link rel="shortcut icon" href="/favicon.ico?v=rMlKOnvxlK">
<meta name="theme-color" content="#ffffff">
Please put the preceding HTML after your favicon declaration, but before your style imports. The order does matter. Browsers load data top down. You want your application's icon to be parsed before the user has to wait for CSS files to be downloaded.
Once your basic branding work has been completed, consider if you'd like to establish a more unique look and feel with theming.
You may further customize Material's look and feel, to achieve a unique experience for your app, by leveraging tools listed on https://material.io/tools and some other tools that I have discovered, which are listed as follows:
There is a wealth of information on https://material.io on the in-depth philosophy behind Material design, with great sections on things like the color system, https://material.io/design/color/the-color-system.html, which dives deep into selecting the right color palette for your brand and other topics such as creating a dark theme for your app.
It is very important to distinguish your brand from other apps or your competitors. Creating a high-quality custom theme will be a time-consuming process; however, the benefits of creating a great first impression with your users are considerable.
Next, we will show you how you can add custom icons to your Angular apps.
Now, let's add your custom branding inside your Angular app. You will need the svg icon you used to create your favicon:
src/assets/img/icons
, named lemon.svg
app.module.ts
, import HttpClientModule
to AppComponent
so that the .svg
file can be requested over HTTPAppComponent
to register the new svg file as an icon:
src/app/app.component.ts
import { MatIconRegistry } from '@angular/material/icon'
import { DomSanitizer } from '@angular/platform-browser'
...
export class AppComponent {
constructor(
iconRegistry: MatIconRegistry,
sanitizer: DomSanitizer
) {
iconRegistry.addSvgIcon(
'lemon',
sanitizer.bypassSecurityTrustResourceUrl(
'assets/img/icons/ lemon.svg'
)
)
}
}
src/app/app.component.ts
template: `
<mat-toolbar color="primary">
<mat-icon svgIcon="lemon"></mat-icon>
<a mat-button routerLink="/home"><h1>LemonMart</h1></a>
</mat-toolbar>
<router-outlet></router-outlet>
`,
Now let's add the remaining icons for menu, user profile, and logout.
Angular Material works out of the box with the Material Design icon font, which is automatically imported into your app as a web font in your index.html
. It is possible to self-host the font; however, if you go down that path, you don't get the benefit if the user's browser has already cached the font from when they visited another website, which could save the speed and latency of downloading a 42-56 KB file in the process. The complete list of icons can be found at https://material.io/icons/.
Now let's update the toolbar with some icons and set up the home page with a minimal template for a fake login button:
<link>
tag has been added to index.html
:
src/index.html
<head>
...
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
Instructions on how to self-host can be found under the Self Hosting section at http://google.github.io/material-design-icons/#getting-icons.
Once configured, working with Material icons is easy.
AppComponent
, update the toolbar to place a Menu button to the left of the title.fxFlex
directive so that the remaining icons are right aligned.src/app/app.component.ts
template: `
<mat-toolbar color="primary">
<button mat-icon-button><mat-icon>menu</mat-icon></button>
<mat-icon svgIcon="lemon"></mat-icon>
<a mat-button routerLink="/home"><h1>LemonMart</h1></a>
<span class="flex-spacer"></span>
<button mat-icon-button>
<mat-icon>account_circle</mat-icon>
</button>
<button mat-icon-button>
<mat-icon>lock_open</mat-icon>
</button>
</mat-toolbar>
<router-outlet></router-outlet>
`,
HomeComponent
, add a minimal template for a login experience, replacing any existing content:
src/app/home/home.component.ts
styles: [`
div[fxLayout] {margin-top: 32px;}
`],
template: `
<div fxLayout="column" fxLayoutAlign="center center">
<span class="mat-display-2">Hello, Limoncu!</span>
<button mat-raised-button color="primary">Login</button>
</div>
`
Your app should look similar to this screenshot:
Figure 7.13: LemonMart with minimal login
There's still some work to be done, in terms of implementing and showing/hiding the menu, profile, and logout icons, given the user's authentication status. We will cover this functionality in Chapter 8, Designing Authentication and Authorization.
In order to debug the router, get a visualization of your routers, and tightly integrate Chrome debugging features using Angular Augury, see Appendix A, Debugging Angular.
Now that you've set up basic routing for your app, we can move on to setting up lazily loaded modules with subcomponents. If you're not familiar with troubleshooting and debugging Angular, please refer to the Appendix A, Debugging Angular, before moving forward.
There are two ways resources are loaded: eagerly or lazily. When the browser loads up the index.html
for your app, it starts processing it top to bottom. First the <head>
element is processed, then the <body>
. For example, the CSS resources we defined in the <head>
of our app will be downloaded before our app is rendered, because our Angular app is defined as a <script>
in the <body>
of the HTML file.
When you use the command ng build
, Angular leverages the webpack module bundler to combine all the JavaScript, HTML, and CSS into minified and optimized JavaScript bundles.
If you don't leverage lazy loading in Angular, the entire contents of your app will be eagerly loaded. The user won't see the first screen of your app until all screens are downloaded and loaded.
Lazy loading allows the Angular build process, working in tandem with webpack, to separate your web application into different JavaScript files called chunks. We can enable this chunking by separating out portions of the application into feature modules. Feature modules and their dependencies can be bundled into separate chunks. Remember that the root module and its dependencies will always be in the first chunk that is downloaded. So, by chunking our application's JavaScript bundle size, we keep the size of the initial chunk at a minimum. With a minimal first chunk, no matter how big your application grows, the time to first meaningful paint remains constant. Otherwise, your app would take longer and longer to download and render as you add more features and functionality to it. Lazy loading is critical to achieving a scalable application architecture.
Consider the following graphic to determine which routes are eagerly loaded and which ones are lazily loaded:
Figure 7.14: Angular router eager vs lazy loading
The rootRouter
defines three routes: a
, b
, and c
. /master
and /detail
represent named router outlets, which are covered in Chapter 12, Recipes – Master/Detail, Data Tables, and NgRx. Route a
is the default route for the app. Routes a
and c
are connected to the rootRouter
with a solid line, whereas route b
is connected using a dashed line. In this context, route b
is configured as a lazy-loaded route. This means that route b
will dynamically load a feature module, called BModule, that contains its childRouter
. This childRouter
can define any number of components, even reusing route names that were reused elsewhere. In this case, b
defines three additional routes: d
, e
, and f
.
Consider the example router definition for the rootRouter
:
rootRouter example
const routes: Routes = [
{ path: '', redirectTo: '/a', pathMatch: 'full' },
{
path: 'a',
component: AComponent,
children: [
{ path: '', component: MasterComponent, outlet: 'master' },
{ path: '', component: DetailComponent, outlet: 'detail' },
],
},
{
path: 'b',
loadChildren:
() => import('./b/b.module')
.then((module) => module.BModule),
canLoad: [AuthGuard],
},
{ path: 'c', component: CComponent },
{ path: '**', component: PageNotFoundComponent },
]
Note that the definitions for routes d
, e
, and f
do not exist in the rootRouter
. See the example router definition for the childRouter
:
childRouter example
const routes: Routes = [
{ path: '', redirectTo: '/b/d', pathMatch: 'full' },
{ path: 'd', component: DComponent },
{ path: 'e', component: EComponent },
{ path: 'f', component: FComponent },
]
As you can see the routes defined in the childRouter
are independent of the ones defined in the rootRouter
. Child routes exist in a hierarchy, where /b
is the parent path. To navigate to the DComponent
, you must use the path /b/d
, whereas, to navigate to CComponent
, you can just use /c
.
Given this example configuration, every component defined in the rootRouter
and their dependencies would be in the first chunk of our app, and thus eagerly loaded. The first chunk would include the components A
, Master
, Detail
, C
, and PageNotFound
. The second chunk would contain the components D
, E
, and F
. This second chunk would not be downloaded or loaded until the user navigated to a path starting with /b
; thus, it's lazily loaded.
In the book I only cover the well-established method of lazily loaded feature modules. Check out John Papa's blog post on creating lazily loading components at https://johnpapa.net/angular-9-lazy-loading-components/.
We will now go over how to set up a feature module with components and routes. We will also use Augury to observe the effects of our various router configurations.
The manager module needs a landing page, as shown in this mock-up:
Figure 7.15: Manager's dashboard
Let's start by creating the home screen for the ManagerModule
:
ManagerHome
component:
$ npx ng g c manager/managerHome -m manager -s -t
In order to create the new component under the manager
folder, we must prefix manager/
in front of the component name. In addition, we specify that the component should be imported and declared with the ManagerModule
. Since this is another landing page, it is unlikely to be complicated enough to require separate HTML and CSS files. You can use --inline-style
(alias -s
) and/or --inline-template
(alias -t
) to avoid creating additional files.
/src
├───app
│ │
│ ├───manager
│ │ │ manager-routing.module.ts
│ │ │ manager.module.ts
│ │ │
│ │ └───manager-home
│ │ │ │ manager-home.component.spec.ts
│ │ │ │ manager-home.component.ts
ManagerHome
component's route with manager-routing.module
, similar to how we configured the Home
component with app-route.module
:
src/app/manager/manager-routing.module.ts
import {
ManagerHomeComponent
} from './manager-home/manager-home.component'
const routes: Routes = [
{ path: '', redirectTo: '/manager/home', pathMatch: 'full' },
{ path: 'home', component: ManagerHomeComponent },
]
Note that http://localhost:5000/manager
doesn't actually resolve to a component yet, because our Angular app isn't aware that ManagerModule
exists. Let's first try the brute-force, eager-loading approach to import ManagerModule
and register the manager route with our app.
Let's start by eagerly loading the ManagerModule
, so we can see how importing and registering routes in the root module doesn't result in a scalable solution:
ManagerModule
in app.module.ts
:
src/app/app.module.ts
import { ManagerModule } from './manager/manager.module'
...
@NgModule({
imports: [..., ManagerModule],
...
})
You will note that http://localhost:5000/manager
still doesn't render its home
component.
Figure 7.16: Router tree with eager loading
Note that at the time of publishing, Augury's support for the Ivy rendering engine is not great. In order to reliably view the Router Tree tab, you need to disable Ivy. You can do so by adding the following setting to the tsconfig.app.json
file in your project:
"angularCompilerOptions": {
"enableIvy": false
}
You will need to restart your Angular app and reload Augury for changes to take effect. However, getting the pretty diagram is not worth accidentally shipping your app with Ivy disabled. Be careful with this one!
/manager
path is correctly registered and pointed at the correct component, ManagerHomeComponent
. The issue here is that the rootRouter
configured in app-routing.module
isn't aware of the /manager
path, so the **
path is taking precedence and rendering the PageNotFoundComponent
instead.'manager'
path in app-routing.module.ts
and assign ManagerHomeComponent
to it, so we can see what happens:
src/app/app-routing.module.ts
import {
ManagerHomeComponent
} from './manager/manager-home/ manager-home.component'
...
const routes: Routes = [
...
{ path: 'manager', component: ManagerHomeComponent },
{ path: '**', component: PageNotFoundComponent },
]
Figure 7.17: Manager home renders with duplicate path registration
As shown in the image above, http://localhost:5000/manager
renders correctly, by displaying manager-home works! However, when you debug the router state through Augury, note that the ManagerHomeComponent
is registered twice. This is because both the rootRouter
and the childRouter
registrations are being picked up. To avoid this issue, we would have to centralize all path creation in the rootRouter
and not use child routers.
Centralizing all paths in the rootRouter
doesn't scale well, because it forces all developers to maintain a single master file to import and configure every module. It is ripe for merge conflicts and frustrating exchanges between team members. As a file grows larger, the chances of introducing a bug increase exponentially, where the same route could unintentionally be registered multiple times.
It is possible to engineer a solution to divide up the modules into multiple files. Instead of the standard *-routing.module
, you could implement a new routes array in ManagerModule
and import it to the rootRouter
. Let's fix the duplicate registration issue.
manager.module.ts
, remove ManagerRoutingModule
from the imports array.manager.module.ts
, implement a Routes
array and set an empty path for the component ManagerHomeComponent
as shown:
src/app/manager/manager.module.ts
import { Routes } from '@angular/router'
export const managerModuleRoutes: Routes = [
{ path: '', component: ManagerHomeComponent }
]
app-routing.module.ts
, import the array you just created and assign it to the children
property of the 'manager'
path:
src/app/app-routing.module.ts
import { managerModuleRoutes } from './manager/manager.module'
...
{ path: 'manager', children: managerModuleRoutes },
Don't forget to remove the component
property and the import for ManagerHomeModule
.
Let's inspect the Router Tree on Augury again to see if we resolved the duplicate registration issue:
Figure 7.18: Router Tree with children routes
The provided solution works. There are no duplicate registrations, because we stopped using the childRouters
in manager-routing.module.ts
. In addition, we maintained some separation of concerns, by not importing ManagerHomeComponent
outside of ManagerModule
, resulting in a more scalable solution. However, as the app grows, we must still register all modules with app.module.ts
. As a result, feature modules are still tightly coupled to the root module in potentially unpredictable ways. Further, this code can't be chunked, because the feature module is directly imported in app.module.ts
, so the TypeScript compiler sees it as a required dependency.
Next, let's transform our configuration into a lazily loading one.
Now that you understand how eager loading of modules works, you will be able to better understand the code we are about to write, which may otherwise seem like black magic, and magical (also known as misunderstood) code always leads to spaghetti architectures.
We will now evolve the eager loading solution to be a lazy loading one. In order to load routes from a different module, we know we can't simply import them, otherwise they will be eagerly loaded. The answer lies in configuring a route using the loadChildren
attribute with an inline import statement informing the router how to load a feature module in app-routing.module.ts
:
app.module.ts
, so remove the ManagerModule
from the imports
.Routes
array added to ManagerModule
.ManagerRoutingModule
to imports
in ManagerModule
.app-routing.module.ts
, implement or update the 'manager'
path with the loadChildren
attribute:
src/app/app-routing.module.ts
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { HomeComponent } from './home/home.component'
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'
...
const routes: Routes = [
...,
{
path: 'manager',
loadChildren:
() => import('./manager/manager.module')
. then(m=> m.ManagerModule),
},
{ path: '**', component: PageNotFoundComponent },
]
...
Lazy loading is achieved via a clever trick that avoids using an import statement at the file level. A function delegate is set to the loadChildren
property, which contains an inline import statement defining the location of the feature module file, such as ./manager/manager.module
, allowing us to refer to ManagerModule
in a type-safe manner without actually fully loading it. The inline import statement can be interpreted during the build process to create a separate JavaScript chunk that can be downloaded only when needed. ManagerModule
then acts as if its own Angular app and manages all its children dependencies and routes.
manager-routing.module
routes, considering that manager
is now their root route:
src/app/manager/manager-routing.module.ts
const routes: Routes = [
{ path: '', redirectTo: '/manager/home', pathMatch: 'full' },
{ path: 'home', component: ManagerHomeComponent },
]
We can now update the route for ManagerHomeComponent
to a more meaningful 'home'
path. This path won't clash with the one found in app-routing.module
, because in this context, 'home'
resolves to 'manager/home'
and, similarly, where path
is empty, the URL will look like http://localhost:5000/manager
.
ng serve
or npm start
command, so Angular can chunk the app properly.http://localhost:5000/manager
.Figure 7.19: Router tree with lazy loading
The root node for ManagerHomeComponent
is now named manager [Lazy].
We have successfully set up a feature module with lazy loading. Next, let's implement the walking skeleton for LemonMart.
Using the site map we created for LemonMart earlier in the chapter, we need to complete the walking skeleton navigation experience for the app. In order to create this experience, we will need to create some buttons to link all modules and components together. We will go at this module by module.
Before we start, update the login button on the HomeComponent
to navigate to the 'manager'
path using the routerLink
attribute and rename the button:
src/app/home/home.component.ts
...
<button mat-raised-button color="primary" routerLink="/manager">
Login as Manager
</button>
...
Now, we can navigate to the ManagerHome
component by clicking on the Login button.
Since we already enabled lazy loading for ManagerModule
, let's go ahead and complete the rest of the navigational elements for it.
In the current setup, ManagerHomeComponent
renders in the <router-outlet>
defined in AppComponent
's template, so when the user navigates from HomeComponent
to ManagerHomeComponent
, the toolbar implemented in AppComponent
remains in place. See the following mock-up for Manager's Dashboard:
Figure 7.20: App-wide and feature module toolbars
The app-wide toolbar remains in place no matter where we navigate to. You can imagine that we can implement a similar toolbar for the feature module that persists throughout ManagerModule
. So, the navigational buttons User Management and Receipt Look-up would always be visible. This allows us to create a consistent UX for navigating subpages across modules.
To implement a secondary toolbar, we need to replicate the parent-child relationship between AppComponent
and HomeComponent
, where the parent implements the toolbar and a <router-outlet>
so that children elements can be rendered in there:
manager
component:
$ npx ng g c manager/manager -m manager --flat -s -t
The --flat
option skips directory creation and places the component directly under the manager
folder, just like app.component
residing directly under the app
folder.
ManagerComponent
, implement a navigational toolbar with activeLink
tracking:
src/app/manager/manager.component.ts
styles: [
`
div[fxLayout] {
margin-top: 32px;
}
`,
`
.active-link {
font-weight: bold;
border-bottom: 2px solid #005005;
}
`,
],
template: `
<mat-toolbar color="accent">
<a mat-button
routerLink="/manager/home"
routerLinkActive="active-link"
>
Manager's Dashboard
</a>
<a mat-button
routerLink="/manager/users"
routerLinkActive="active-link"
>
User Management
</a>
<a mat-button
routerLink="/manager/receipts"
routerLinkActive="active-link"
>
Receipt Lookup
</a>
</mat-toolbar>
<router-outlet></router-outlet>
`
It must be noted that feature modules don't automatically have access to services or components created in parent modules. This is an important default behavior to preserve a decoupled architecture. However, there are certain cases where it is desirable to share some amount of code. In this case, mat-toolbar
needs to be reimported. Since the MatToolbarModule
is already loaded in src/app/material.module.ts
, we can just import this module into manager.module.ts
and there will not be a performance or memory penalty for doing so.
ManagerComponent
is declared and MaterialModule
is imported in ManagerModule
:
src/app/manager/manager.module.ts
import { MaterialModule } from '../material.module'
import { ManagerComponent } from './manager.component'
...
declarations: [..., ManagerComponent],
imports: [..., MaterialModule],
$ npx ng g c manager/userManagement -m manager
$ npx ng g c manager/receiptLookup -m manager
example
{ path: '', redirectTo: '/manager/home', pathMatch: 'full' },
{ path: 'home', component: ManagerHomeComponent },
{ path: 'users', component: UserManagementComponent },
{ path: 'receipts', component: ReceiptLookupComponent },
In order to target the <router-outlet>
defined in ManagerComponent
, we need to create a parent route first and then specify routes for the subpages:
src/app/manager/manager-routing.module.ts
...
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ManagerHomeComponent } from './manager-home/manager-home.component'
import { ManagerComponent } from './manager.component'
import { ReceiptLookupComponent } from './receipt-lookup/receipt-lookup.component'
import { UserManagementComponent } from './user-management/user-management.component'
const routes: Routes = [
{
path: '',
component: ManagerComponent,
children: [
{ path: '', redirectTo: '/manager/home', pathMatch: 'full' },
{ path: 'home', component: ManagerHomeComponent },
{ path: 'users', component: UserManagementComponent },
{ path: 'receipts', component: ReceiptLookupComponent },
],
},
]
You should now be able to navigate through the app. When you click on the Login as Manager button, you will be taken to the page shown here. The clickable targets are highlighted, as shown:
Figure 7.21: Manager's Dashboard with all router links highlighted
If you click on LemonMart, you will be taken to the home page. If you click on Manager's Dashboard, User Management, or Receipt Lookup, you will be navigated to the corresponding subpage, while the active link will be bold and underlined on the toolbar.
Upon login, users will be able to access their profiles and view a list of actions they can access in the LemonMart app through a side navigation menu. In Chapter 8, Designing Authentication and Authorization, when we implement authentication and authorization, we will be receiving the role of the user from the server. Based on the role of the user, we will be able to automatically navigate or limit the options users can see. We will implement these components in this module so that they will only be loaded once a user is logged in. For the purpose of completing the walking skeleton, we will ignore authentication-related concerns:
$ npx ng g c user/profile -m user
$ npx ng g c user/logout -m user -t -s
$ npx ng g c user/navigationMenu -m user -t -s
Start with implementing the lazy loading in app-routing.module.ts
:
src/app/app-routing.module.ts
...
{
path: 'user',
loadChildren:
() => import('./user/user.module')
.then(m => m.UserModule),
},
Now implement the child routes in user-routing.module.ts
:
src/app/user/user-routing.module.ts
...
const routes: Routes = [
{ path: 'profile', component: ProfileComponent },
{ path: 'logout', component: LogoutComponent },
]
We are implementing routing for NavigationMenuComponent
, because it'll be directly used as an HTML element. In addition, since UserModule
doesn't have a landing page, there's no default path defined.
AppComponent
, wire up the user and logout icons:
src/app/app.component.ts
...
<mat-toolbar>
...
<button
mat-mini-fab routerLink="/user/profile"
matTooltip="Profile" aria-label="User Profile"
>
<mat-icon>account_circle</mat-icon>
</button>
<button
mat-mini-fab routerLink="/user/logout"
matTooltip="Logout" aria-label="Logout"
>
<mat-icon>lock_open</mat-icon>
</button>
</mat-toolbar>
Icon buttons can be cryptic, so it's a good idea to add tooltips to them. In order for tooltips to work, switch from the mat-icon-button
directive to the mat-mini-fab
directive and ensure that you import MatTooltipModule
in material.module.ts
. In addition, ensure that you add aria-label
for icon-only buttons so that users with disabilities relying on screen readers can still navigate your web application.
You'll note that the two buttons are too close to each other, as follows:
Figure 7.22: Toolbar with icons
fxLayoutGap="8px"
to <mat-toolbar>
; however, now the lemon logo is too far apart from the app name, as shown:Figure 7.23: Toolbar with padded icons
src/app/app.component.ts
...
<mat-toolbar>
...
<a mat-icon-button routerLink="/home">
<mat-icon svgIcon="lemon"></mat-icon>
<span class="mat-h2">LemonMart</span>
</a>
...
</mat-toolbar>
As shown in the following screenshot, the grouping fixes the layout issue:
Figure 7.24: Toolbar with grouped and padded elements
This is more desirable from a UX perspective also; now users can go back to the home page by clicking on the lemon as well.
Our walking skeleton presumes the role of the manager. To be able to access all components we are about to create, we need to enable the manager to be able to access POS and inventory modules.
Update ManagerComponent
with two new buttons:
src/app/manager/manager.component.ts
<mat-toolbar color="accent" fxLayoutGap="8px">
...
<span class="flex-spacer"></span>
<button
mat-mini-fab routerLink="/inventory"
matTooltip="Inventory" aria-label="Inventory"
>
<mat-icon>list</mat-icon>
</button>
<button
mat-mini-fab routerLink="/pos"
matTooltip="POS" aria-label="POS"
>
<mat-icon>shopping_cart</mat-icon>
</button>
</mat-toolbar>
Note that these router links will navigate us out of the realm of the ManagerModule
, so it is normal for the manager-specific secondary toolbar to disappear.
Now, it'll be up to you to implement the last two remaining modules. For the two new modules, I provide high-level steps and refer you to a previous module you can model the new one on. If you get stuck refer to the projects/ch7
folder on the GitHub project at https://github.com/duluca/lemon-mart.
PosModule
is very similar to the UserModule
, except that PosModule
was a default path. The PosComponent
will be the default component. This has the potential to be a complicated component with some subcomponents, so don't use inline templates or styles:
PosComponent
PosComponent
as the default pathPosModule
Now let's implement the inventory module.
InventoryModule
is very similar to ManagerModule
, as shown:
Figure 7.25: Inventory Dashboard mock-up
Inventory
componentMaterialModule
inventory-routing.module.ts
InventoryModule
InventoryModule
navigation in InventoryComponent
Figure 7.26: LemonMart Inventory Dashboard
Now that the walking skeleton of the app is completed, it is important to inspect the router tree to ensure that lazy loading has been configured correctly and modules aren't unintentionally being eagerly loaded.
Navigate to the base route of the app and use Augury to inspect the router tree, as illustrated:
Figure 7.27: Router tree with lazy loading
Everything but the initially required components should be denoted with the [Lazy] attribute. If, for some reason, routes are not denoted with [Lazy], chances are that they are mistakenly being imported in app.module.ts
or some other component.
In your router tree, you may notice that ProfileComponent
and LogoutComponent
are eagerly loaded, whereas the UserModule
is correctly labeled as [Lazy]. Even multiple visual inspections through the tooling and the codebase may leave you searching for the culprit. However, if you run a global search for UserModule
, you'll quickly discover that it was being imported into app.module.ts
. When running CLI commands, your module may inadvertently get re-imported into app.module.ts
, so keep an eye out for this!
To be on the safe side, inspect your app.module.ts
file and be sure to remove any import statements for modules or components that are not at the root level. Your file should look like the following one:
src/app/app.module.ts
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { FlexLayoutModule } from '@angular/flex-layout'
import { BrowserModule } from '@angular/platform-browser'
import {
BrowserAnimationsModule
} from '@angular/platform-browser/ animations'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { HomeComponent } from './home/home.component'
import { MaterialModule } from './material.module'
import {
PageNotFoundComponent
} from './page-not-found/page-not-found.component'
@NgModule({
declarations: [AppComponent, HomeComponent, PageNotFoundComponent],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MaterialModule,
HttpClientModule,
FlexLayoutModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
If you disabled Ivy while debugging your routes with Augury, now's the time to re-enable it.
It is expected that the reader resolves any testing errors before moving on. Ensure that npm test
and npm run e2e
execute without errors.
Now that we have a lot of modules to deal with, it becomes tedious to configure the imports and providers for each spec file individually. For this purpose, I recommend creating a common testing module to contain generic configuration that you can reuse across the board.
First start by creating a new .ts
file
common/common.testing.ts
.I have provided fake implementations of ObservableMedia
, MatIconRegistry
, and DomSanitizer
, along with arrays for commonTestingProviders
and commonTestingModules
:
src/app/common/common.testing.ts
import {
HttpClientTestingModule
} from '@angular/common/http/ testing'
import { SecurityContext } from '@angular/core'
import { MediaChange } from '@angular/flex-layout'
import { ReactiveFormsModule } from '@angular/forms'
import {
SafeResourceUrl,
SafeValue
} from '@angular/platform-browser'
import {
NoopAnimationsModule
} from '@angular/platform-browser/animations'
import { RouterTestingModule } from '@angular/router/testing'
import { Observable, Subscription, of } from 'rxjs'
import { MaterialModule } from '../material.module'
const FAKE_SVGS = {
lemon: '<svg><path id="lemon" name="lemon"></path></svg>',
}
export class MediaObserverFake {
isActive(query: string): boolean {
return false
}
asObservable(): Observable<MediaChange> {
return of({} as MediaChange)
}
subscribe(
next?: (value: MediaChange) => void,
error?: (error: any) => void,
complete?: () => void
): Subscription {
return new Subscription()
}
}
export class MatIconRegistryFake {
// tslint:disable-next-line: variable-name
_document = document
addSvgIcon(iconName: string, url: SafeResourceUrl): this {
// this.addSvgIcon('lemon', 'lemon.svg')
return this
}
getNamedSvgIcon(name: string, namespace: string = ''): Observable<SVGElement> {
return of(this._svgElementFromString(FAKE_SVGS.lemon))
}
private _svgElementFromString(str: string): SVGElement {
const div = (this._document || document)
. createElement('DIV')
div.innerHTML = str
const svg = div.querySelector('svg') as SVGElement
if (!svg) {
throw Error('<svg> tag not found')
}
return svg
}
}
export class DomSanitizerFake {
bypassSecurityTrustResourceUrl(url: string): SafeResourceUrl {
return {} as SafeResourceUrl
}
sanitize(
context: SecurityContext,
value: SafeValue | string | null):
string | null
{
return value?.toString() || null
}
}
export const commonTestingProviders: any[] = [
// Intentionally Left Blank!!!
]
export const commonTestingModules: any[] = [
ReactiveFormsModule,
MaterialModule,
NoopAnimationsModule,
HttpClientTestingModule,
RouterTestingModule,
]
Now let's see a sample use of this shared configuration file:
src/app/app.component.spec.ts
import { MediaObserver } from '@angular/flex-layout'
import { MatIconRegistry } from '@angular/material/icon'
import { DomSanitizer } from '@angular/platform-browser'
...
import {
DomSanitizerFake,
MatIconRegistryFake,
MediaObserverFake,
commonTestingModules,
} from './common/common.testing'
...
TestBed.configureTestingModule({
imports: commonTestingModules,
providers: commonTestingProviders.concat([
{ provide: MediaObserver, useClass: MediaObserverFake },
{ provide: MatIconRegistry, useClass: MatIconRegistryFake },
{ provide: DomSanitizer, useClass: DomSanitizerFake },
]),
declarations: [AppComponent],
...
Most other modules will just need commonTestingModules
to be imported.
Stop! Did you ensure all your unit tests are passing? To ensure that your tests are always passing implement a CI pipeline in CircleCI, as demonstrated in Chapter 4, Automated Testing, CI, and Releasing to Production.
With your tests up and running, the walking skeleton for LemonMart is completed. Now, let's look ahead and start thinking about what kinds of data entities we might be working with.
The fourth step in router-first architecture is achieving a stateless, data-driven design. To achieve this, it helps a lot to organize your APIs around major data components. This will roughly match how you consume data in various components in your Angular application. We will start off by defining our major data components by creating a rough data entity relationship diagram (ERD). In Chapter 10, RESTful APIs and Full-Stack Implementation, we will design and implement an API for the user data entity using Swagger.io and Express.js.
Let's start by taking a stab at what kind of entities you would like to store and how these entities might relate to one another.
Here's a sample design for LemonMart, created using draw.io:
Figure 7.28: ERD for LemonMart
At this moment, whether your entities are stored in a SQL or NoSQL database is inconsequential. My suggestion is to stick to what you know, but if you're starting from scratch, a NoSQL database like MongoDB will offer the most flexibility as your implementation and requirements evolve.
Generally speaking, you will need CRUD APIs for each entity. Considering these data elements, we can also imagine some user interfaces around these CRUD APIs. Let's do that next.
Mock-ups are important in determining what kind of components and user controls we will need throughout the app. Any user control or component that will be used across components will need to be defined at the root level and others scoped with their own modules.
Earlier in this chapter, we identified the sub modules and designed landing pages for them to complete the walking skeleton. Now that we have defined the major data components, we can complete mock-ups for the rest of the app. When designing screens at a high level, keep several things in mind:
Keep in mind that there's no one right way to design any user experience, which is why when designing screens, you should always keep modularity and reusability in mind.
As mentioned earlier in the chapter, it is important to document every artifact you create. Wikis offer a way to create living documentation that can be collaboratively updated or edited. While Slack, Teams, email, and whiteboards offer good collaboration opportunities, their ephemeral nature leaves a lot to be desired.
So, as you generate various design artifacts, such as mock-ups or design decisions, take care to post them on a wiki reachable by all team members:
Figure 7.29: GitHub.com LemonMart wiki
You can see a summary view of the wiki here:
Figure 7.30: Summary view of LemonMart mock-ups
Now that your artifacts are in a centralized place, it is accessible by all team members. They can add, edit, update, or groom the content. This way your wiki becomes useful, living documentation of the information that your team needs, as opposed to a piece of documentation you feel like you're being forced to create. Raise your hand if you've ever found yourself in that situation!
Next, integrate your mock-ups into your app, so you can collect early feedback from your stakeholders and test out the flow of your application.
Place the mock-ups in the walking skeleton app so that testers can better envision the functionality that is yet to be developed. See an example of this idea in action here:
Figure 7.31: Using mock-ups in the UI to verify flow of app
This will also be helpful when designing and implementing your authentication and authorization workflow. With the mock-ups completed, we can now continue the implementation of LemonMart's authentication and authorization workflow in Chapter 8, Designing Authentication and Authorization.
In this chapter, you mastered how to effectively use the Angular CLI to create major Angular components and scaffolds. You became familiar with the 80-20 rule. You created the branding of your app, leveraging custom and built-in Material iconography. You learned how to debug complicated router configurations with Augury. Finally, you began building router-first apps, defining user roles early on, designing with lazy loading in mind, and nailing down a walking-skeleton navigation experience early on. We went over designing around major data entities. We also covered the importance of completing and documenting high-level UX design of our entire app so that we can properly design a great conditional navigation experience.
To recap, in order to pull off a router-first implementation, you need to do this:
In this chapter, you executed steps 1-3; in the next four chapters, you will execute steps 4-7. In Chapter 8, Designing Authentication and Authorization, we will tap into OOP design and inheritance and abstraction, along with a deep dive into security considerations and designing a conditional navigation experience. In Chapter 10, RESTful APIs and Full-Stack Implementation, you will see a concrete full-stack implementation using the Minimal MEAN stack. Chapter 11, Recipes – Reusability, Routing, and Caching, and Chapter 12, Recipes – Master/Detail, Data Tables, and NgRx, we will tie everything together by sticking to a decoupled component architecture, smartly choosing between creating user controls and components, and maximizing code reuse with various TypeScript, RxJS, and Angular coding techniques.
Answer the following questions as best as you can to ensure that you've understood the key concepts from this chapter without Googling. Do you need help answering the questions? See Appendix D, Self-Assessment Answers online at https://static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf or visit https://expertlysimple.io/angular-self-assessment.