-
Notifications
You must be signed in to change notification settings - Fork 2
Angular Modules
As we've seen throughout this tutorial, Angular chapters are a key part of any Angular application. Welcome back to Angular: Getting Started, My name is Deborah Kurata, and in this tutorial chapter, we focus on Angular Modules and how to use them to better organize our code. Ooh, it's so beautiful. No, it's not a Van Gogh, not even a Picasso. And yeah, it sort of looks like a metro map. This is a picture of what our application could look like if we leverage the power of Angular chapters. Instead of one large AppModule like we have now, there are multiple chapters, and each piece of our application has a logical place within one of those chapters. This keeps each chapter smaller and more manageable. In this tutorial chapter, we take another look at the definition and purpose of an Angular chapter. We then focus in on the Angular chapter metadata to better understand how to use it. We leverage that knowledge to create a feature chapter for our application, and take it one step further defining a shared chapter to reduce duplication. Lastly, we re-examine our application's root Angular chapter. Let's get started.
What is an Angular chapter? As we've seen earlier in this tutorial, an Angular chapter is a class with an NgModule decorator. Its purpose? To organize the pieces of our application. Arrange them into cohesive blocks of functionality and extend our application with capabilities from external libraries. Angular chapters provide the environment for resolving the directives and pipes in our components' templates. We'll talk more about this in a few moments. And chapters are a great way to selectively aggregate classes from other chapters and re-export them in a consolidated convenience chapter. BrowserModule, HttpModule, and RouterModule are all examples of this, and we'll create our own convenience chapter when we build a shared chapter a little later. An Angular chapter can be loaded eagerly when the application starts, or it can be lazy loaded asynchronously by the router. Lazy loading is out of the scope of this tutorial, but is discussed in detail in my Angular Routing tutorial, here on Pluralsight. How does an Angular chapter organize our application? An Angular chapter declares each component, directive, and pipe that it manages. Every component directive and pipe we create belongs to an Angular chapter. An Angular chapter bootstraps our route application component, defining the component needed to display our first template. An Angular chapter can export components, directives, pipes, and even other Angular chapters, making them available for other chapters to import and use. An Angular chapter imports other Angular chapters. This brings in the exported functionality from those imported chapters. An Angular chapter can register service providers with the Angular injector, making the services available to any class in the application. We can think of an Angular chapter as a box. Inside that box we declare each of our components. If those components need any functionality, that functionality also needs to be defined within this box. The AppComponent sets up the routing for our main menu using routerLink, and includes the router-outlet directive, so it needs the routing directive's router service and routes, which are defined in RouterModule. The Product-List Component uses ngModel, so it needs the FormsModule. The product list component also uses ngFor and ngIf, so it needs the BrowserModule. The product list component uses the pipe, so it needs that too. The product list component also uses the star rating components directive, so it needs that as well, and so on until the box contains everything that each of our components needs. Saying this another way, for each component that belongs to an Angular chapter, that Angular chapter provides the environment for template resolution. The chapter defines which set of components, directives, and pipes are available to the component's template. Each declared component's template is resolved using only the capabilities provided within that chapter. Let's look at our product list component as an example. The product list components template uses ngModel, so ngModel must be available within this chapter. We achieve that by importing the Angular FormsModule. The product list components template also uses a directive we created, the StarComponent, so the StarComponent must be available within this chapter. Since the StarComponent is one we created, we can either declare the StarComponent within the chapter directly, or we can import another chapter that exports the StarComponent. Importing an Angular chapter brings in the functionality exported by that chapter. And we need to do one or the other, never both. We didn't need to think about template resolution much in our sample application up until now because all of the pieces of our application are in one Angular chapter. But we will need to keep this in mind as we split our application into multiple Angular chapters. Let's take a quick look at our current AppModule. Here is the Angular chapter we defined throughout this tutorial. It is the application's root Angular chapter, and by convention is called AppModule. The AppModule imports the system Angular chapters we need, including the RouterModule, which is where we configured our routes. It declares each component and pipe that we created in this tutorial, and it bootstraps the application with the root application component, AppComponent. We have a lot of information in here, and we're mixing up basic application pieces, such as our welcome component, with pieces specific to our product feature. Let's journey through the ngModule metadata to better understand how Angular chapters work, so we can then refactor our AppModule into multiple chapters for better code organization.
As we have seen, every Angular application has at least one Angular chapter called the root application chapter, or AppModule. And an Angular application has at least one component, called the root application component, or AppComponent. The AppModule bootstraps the AppComponent to provide the directive used in the index.html file. We covered the bootstrapping process in the Introduction To Components chapter earlier in this tutorial. The bootstrap array of the ngModule decorator defines the component that is the starting point of the application. This is the component that is loaded when the application is launched. Here are some things to keep in mind when using the bootstrap array. Every application must bootstrap at least one component, the root application component. We do this by simply adding the root application component to the bootstrap array of the root application chapter. The bootstrap array should only be used in the root application chapter, AppModule. As we build other Angular chapters, we won't use the bootstrap array.
Every component directive and pipe we create is declared by an Angular chapter. We use the declarations array of the ngModule decorator to define the components, directives, and pipes that belong to this Angular chapter. Here are some things to keep in mind when using the declarations array. Every component directive and pipe we create has to belong to one and only one Angular chapter. In our sample application, all of our components are defined in one Angular chapter, AppModule. It would be better to divide the components into multiple chapters, with basic application pieces in the AppModule and feature pieces and appropriate feature chapters. We'll do that a little later in this tutorial chapter. As we separate out our pieces, it is important to remember that each component, directive, and pipe belongs to one and only one Angular chapter. Only declare components, directives, and pipes. Don't add other classes, services or chapters to the declarations array. Never redeclare components, directives or pipes that belong to another chapter. This is a corollary to truth number one. If we redeclare, then the component directive or pipe no longer belongs to one and only one Angular chapter. For example, the StarComponent directive belongs to Module B, so we should never redeclare StarComponent in Module A. We should only declare components, directives, and pipes that belong to this chapter. All declared components, directives, and pipes are private by default. They are only accessible to other components, directives, and pipes declared in the same chapter. So if we declare the StarComponent in Module B, by default that component is not available to components in other Angular chapters. We share components, directives, and pipes by exporting them. We'll talk more about exporting in a few moments. The Angular chapter provides the template resolution environment for its component's templates. When we include a component in the declarations array of an Angular chapter, the component belongs to that Angular chapter. That component's template, directives, and pipes are then resolved within that chapter. When we use a directive in a component's template, Angular looks to the chapter for the definition of that directive. If the component defining that directive is not declared within the same Angular chapter or exported from an imported chapter, Angular won't find the directive and will generate an error. For this example, the StarComponent must be declared in the same chapter as the product list component, or the StarComponent must be exported from an imported chapter, never both.
The exports array of the ngModule decorator allows us to share an Angular chapter's components, directives, and pipes with other chapters. We can export any of this chapter's components, directives, and pipes so they can be pulled in when another chapter imports this chapter. We can also re-export system Angular chapters, such as FormsModule and HttpModule. We can re-export third-party chapters such as material design. Material design is a set of high-quality user interface components, including buttons and dialogs. And we can re-export our own chapters. Here are some things to keep in mind when using the exports array. Export any component, directive or pipe if another component needs it. A chapter can export any of its declared components, directives or pipes. Re-export chapters to re-export their components, directives, and pipes. This is useful when consolidating features for multiple chapters to build a convenience or shared chapter. We can re-export something without importing it first. An Angular chapter only needs to import the components, directives, and pipes that are required by the components declared in the chapter. But the Angular chapter can still provide capabilities to other chapters that import it by re-exporting. In this example, a shared chapter exports the FormsModule even though it did not import it. So any chapter that imports the shared chapter will have access to the ngModel and other forms directives. We'll see this in an upcoming demo. Never export a service. Services added to the providers array of an Angular chapter are registered with the root application injector, making them available for injection into any class in the application. So there is no point in exporting them, they are already shared throughout the application.
An Angular chapter can be extended by importing capabilities from other Angular chapters. The imports array of the ngModule decorator allows us to import supporting chapters that export components, directives or pipes. We then use those exported components, directives, and pipes within the templates of components that are declared in this chapter. Many Angular system libraries are Angular chapters, such as the FormsModule and HttpModule we've used in this tutorial. We can import Angular chapters to use their capabilities. Many third-party libraries are also Angular chapters, such as material design. We can import third-party Angular chapters to use their capabilities. We can import our own chapters to extend our application with additional features or share capabilities across several chapters. We'll see that in an upcoming demo. And we could separate out our route configurations into its own chapter or set of chapters and import that. Here are some things to keep in mind when using the imports array. Importing a chapter makes available any exported components, directives, and pipes from that chapter. Recall that we are using ngModel in our product list component for two-way binding. The ngModel directive is exported in the FormsModule. By importing the FormsModule into our AppModule, we can use ngModel in any component declared in our AppModule. Only import what this chapter needs. Only import chapters whose exported components, directives or pipes are needed by this chapter's component templates. Don't import anything this chapter does not need. Importing a chapter does not provide access to its imported chapters. Hmm, let's look at that with the picture. Here we have AppModule, which declares the product list component, and a shared chapter that declares and exports the StarComponent. AppModule imports the shared chapter, so the shared chapter's exports are available to the AppModule's component templates. This means that the product list component can use the StarComponent directive. If the shared chapter imports FormsModule, then the FormsModule's exports are available to the shared chapter, and the StarComponent could use the ngModel directive. But the FormsModule exports are not available to the AppModule, so the product list component could not use the ngModel directive. I've heard this rule also stated another way, imports are not inherited. Note, however, that if the shared chapter re-exported the FormsModule, then the FormsModule exports are available to the AppModule. And the product list component could use the ngModel directive. So when thinking about the relationship between chapters, think of a chapter more as a box than as a tree structure.
Angular chapters can also register service providers for our application. However, this is no longer recommended practice. Starting with Angular version 6, the recommended way to register service providers for our application is to use the providedIn property of the service itself, not the provider's array of the Angular chapter. Because you may see older code, use the providers array to register services. I'll still cover it. Here are some things to keep in mind when using the ngModule providers array. Any service provider added to the providers array is registered at the root of the application, so the service is available to be injected into any class in the application. Say for example we have a feature chapter called ProductModule. We add the product service to the providers array of this chapter. At first glance we may think we have encapsulated the product service into the ProductModule, but that is not the case. Any service provider added to the providers array is registered at the root of the application and is available to any class, even classes and other feature chapters. So if we want to ensure a particular service is encapsulated and only accessible within a specific component or set of components, add the service provider to the providers array of an appropriate component instead of an Angular chapter. Note that this is not the case for lazy loaded services. See the Angular documentation for more information on lazy loading. Don't add services to the providers array of a shared chapter. As discussed in the Services and Dependency Injection tutorial chapter, there should only be one instance of a service that is an application-wide singleton. So a service should not be included in the providers array for any chapter that is meant to be shared. Instead, consider building a core chapter for services and importing it once in the AppModule. This will help ensure that the services are only registered one time. We could even add code to the code chapter's constructor to ensure that it is never imported a second time. See the Angular documentation for details. Now that we've covered the basics of the ngModule decorator, let's refactor our application into multiple Angular chapters.
So far in this tutorial, we created the route application chapter, AppModule. It declares all of our components and our pipe. It imports the system Angular chapters that our components need. But this is getting a little unwieldy. We have no separation of responsibilities. Here we are mixing our basic application features, such as the welcome component, with our product features, such as the product components, with our shared features, such as the StarComponent. As we add more feature sets to this application, such as customer management, invoicing, and so on, this is only going to get harder to manage. So let's reorganize and refactor to break this into multiple Angular chapters. The first thing we want to do is extract some of these pieces into feature sets. We can then create a feature chapter for each feature set. Using feature chapters helps us partition our application into logical groupings, with separate concerns. Our first step is to define a new feature chapter. Creating a feature chapter involves defining a new chapter file, ProductModule in this example, and reorganizing the pieces of the application so that all of the associated feature pieces and everything they need are here in this chapter. In the declarations array of the feature chapter, we add the appropriate components that provide the features for the application. In this example, we add the product list component and product detail component. Then as we did with the box example at the beginning of this tutorial chapter, we start to look at what each component needs. In this example, the product list component uses the pipe, so we need that. And both the product list and product detail components use the StarComponent, so we'll need that here as well. But that's not enough. The product list Component uses ngModel and ngFor, and both components use ngIf and routing. How do we get that? We import these needed capabilities from other Angular chapters. Our product components use routing, so we import the system RouterModule. The product list component uses ngModel, so we import the system FormsModule. And we need ngFor and ngIf, so do we pull in the system BrowserModule? Nope, the BrowserModule should only be imported into the root application chapter, AppModule. Instead, we import the system CommonModule. The CommonModule exposes the ngFor and ngIf directives. Not surprisingly, the BrowserModule itself actually imports and exports the CommonModule, which is why we have access to ngFor and ngIf when we import BrowserModule in our AppModule. Our feature chapter is looking pretty good here, but now that we've removed these features from the root application chapter, how will the application find all of these features? What's that, imports array did you say? That's correct. We need to import the ProductModule into the AppModule. That extends the AppModule with the ProductModule features. Want to try this out?
In this demo, we'll build a feature chapter for our product features. We are back in the sample application. Here is our AppModule. Let's create a new feature chapter for our product feature. Want to try generating it with the Angular CLI? We open the integrated terminal and type ng for the Angular CLI, g for generate, m for chapter, and the name of our chapter. Since we are creating the ProductModule, we want it in the products folder, so products/product. That's all that's required. But by default, Angular will create a new folder for this chapter because it rightly assumes that we'll create the chapter when we define the feature and the feature folder. But we already have the products folder, so we'll use the --flat option, and we want to import this chapter into the AppModule to pull in its functionality, so we use the -m flag, specifying the chapter name. Press Enter and we see that the CLI created the chapter. It also updated our AppModule. We can see that here. It added ProductModule to our imports array. Yay! Let's open the new ProductModule. The CLI already created the class with the NgModule decorator and the required import statements. Since this chapter is for our product features, in the declarations array we add the ProductListComponent, ProductDetailComponent, ConvertToSpacesPipe, and StarComponent. Now we can remove these declarations from the AppModule. Going back to our ProductModule, we can see that the CLI already included CommonModule here, since we need that in every feature chapter. We will add the FormsModule and RouterModule. Now we can remove the FormsModule from the AppModule and its associated import statement. When we added the RouterModule to the imports array in the AppModule, we called forRoot to pass in the configured routes for our root component. Now that we are adding the RouterModule to the imports array of a feature chapter, we don't call forRoot, rather, we call forChild, and there we pass in the routes related to products. Let's cut the product routes from the AppModule and paste them here in our ProductModule. And we need to import the ProductDetailGuard. Recall that the RouterModule registers the router service provider, declares the router directives, and exposes our configured routes. But as we've discussed previously, we never want to register a service more than once. So when we use forRoot to pass in our configured routes, the RouterModule knows to register the router service provider. When we use forChild, as we did here, the RouterModule knows not to re-register the router service. Note that we could also consider moving the routes into their own chapters. We'll look at that a little later. Do you think our application will run? And our application works as expected. So we now have our first working feature chapter. But let's think about this for a moment. As we build our application, we'll build more features. Each logical set of features will have their own feature chapter, and each feature chapter will most likely need the CommonModule for common directives such as ngFor and ngIf, the FormsModule for ngModel and two-way binding, and we may have other features that want to reuse our StarComponent. Do we really want to repeat all of this in each feature chapter? There has to be a better way. Yep, we can define a SharedModule.
The purpose of a SharedModule is to organize a set of commonly used pieces into one chapter and export those pieces so they are available to any chapter that imports the SharedModule. This allows us to selectively aggregate our reusable components and any external chapters and re-export them in a consolidated convenience chapter. Creating a SharedModule involves defining a new chapter file, SharedModule in this example, and reorganizing the pieces of the application so that the shared pieces are here in this chapter. First, we add the components, directives, and pipes that we want to share throughout our application to the declarations array. In this example, we only want to add the StarComponent. Then we add to the imports array anything that this shared component needs. In this example, we import the CommonModule because our StarComponent may need it. We don't import FormsModule because we don't need it here. If our StarComponent did use two-way binding or we added another component here that did, we'd need to import FormsModule as well. We then need to export everything that we want to share. The exports array defines what this Angular chapter shares with any chapter that imports it. We export the StarComponent. That way it is available to the components and any chapter that imports the shared chapter. We re-export the CommonModule and FormsModule so their directives and other features are available to any chapter that imports the SharedModule. And notice here that we can export something without importing it first. To use the SharedModule, we import it into every feature chapter that needs the shared capabilities, such as our ProductModule. Let's give this a try. We want to build a shared chapter, and we'll again use the CLI. Do you recall the correct CLI command to generate a chapter? We type ng for the Angular CLI, g for generate, m for chapter, and the name of our chapter. Since we are creating the shared chapter, we want it in the shared folder, so shared/shared. We already have the shared folder in place, so we'll specify the --flat option, that way the CLI won't create another folder. And we want to import this chapter into the product chapter to pull in its functionality, so we use the -m flag, specifying the chapter path and name. Press Enter, and we see that the CLI creates the shared chapter and it updates our product chapter. We can see that here, it added shared chapter to our imports array. Let's open the new shared chapter. The CLI already created the class with the NgModule decorator and the required import statements, and it included CommonModule in the imports array here. Now what did we want to share? Well, we want to share the StarComponent, so we add that to the declarations array here. To share the StarComponent, we need to export it. Let's add an exports array and export the StarComponent. There's a few more things that we want to share. So we don't have to import them into every feature chapter, we'll add CommonModule and FormsModule to the exports array. If there were other chapters we wanted to share, such as reactive forms chapter or material design, we could add them here as well. We could also share the ConvertToSpacesPipe. I'll leave that up to you to add here if you wish. Now we can remove the StarComponent, CommonModule, and FormsModule from the ProductModule, along with their associated import statements since these are now already accessible from the imported SharedModule. Are we good to go? Yep, our application comes up as it did before. Looking back at our code, notice now that our feature chapter, ProductModule, only contains product pieces and the shared chapter. And the SharedModule is clean only including the pieces we want to share. We can reuse the SharedModule and any future feature chapters as we add functionality to our application.
We now know that every application has a root application chapter that is, by convention, called AppModule. The main purpose of the AppModule is to orchestrate the application as a whole. And now that we've removed the feature and shared pieces from this chapter, its purpose is easier to see. Let's take another look. We've reduced the code in AppModule such that it now fits on one page. The AppModule normally imports BrowserModule. This is the chapter that every browser application must import. BrowserModule registers critical application service providers. It also imports and exports CommonModule, which declares an exports directive such as ngIf and ngFor. These directives are then available to any of the AppModule's component templates. We also import HttpModule to register the angular HTTP client service provider. We import RouterModule and call forRoot, passing in the configured routes for the root of the application. Here we configure our default route and any wildcard routes. Then we import each feature chapter. In this example we have only one feature chapter, ProductModule. The declarations array identifies the list of components that belong to this chapter. In this example the root component, AppComponent, and the application's WelcomeComponent are declared here. The bootstrap array identifies the root component, AppComponent, as the bootstrap component. When Angular launches the application, it loads this component and displays its template. We could take the refactoring a step further and separate the routing into its own chapter. We could create one Angular chapter for our root application routes, and another Angular chapter for our product feature routes. Let's go back to the slides and see what that code would look like. If we wanted to refactor our root application routes into their own chapter, this is what it might look like. We export a class, add the NgModule decorator, and import what we need. We add the RouterModule to the imports array, passing in our root application routes, including our default route and our wildcard route. Notice that we call forRoot here to ensure that we register the routing service provider, and we export RouterModule so we can use it from any chapter that imports this chapter. We import the AppRoutingModule in the AppModule here. Note that the AppRoutingModule is listed after the ProductModule in the imports array. This is required because Angular registers the routes based on the order of the chapters specified here. The ProductModule is listed first, so it registers the product routes first. Then the AppRoutingModule registers the application routes, including the wildcard route. If the AppRoutingModule was before the ProductModule, then the wildcard route would be registered before the product route's, and the product route's would never be accessible. So the AppRoutingModule with the wildcard route should always be last in this list. We can do the same to refactor our product feature routes into their own chapter. The key difference here is when we import RouterModule and any feature chapter, we pass the configured routes to the forChild method instead of the forRoot method. This ensures that we don't register the routing service provider a second time. And we import this product routing chapter into the ProductModule, as shown here. Now let's finish up this tutorial chapter with some checklists and a summary.
Your application architecture depends on many factors, including the size and scope of the application you are working on, your team size and experience, and your project's goals. But here are some suggestions based on what we covered in this tutorial chapter. Every application must always have a root application chapter, by convention called AppModule. This is normally the chapter that bootstraps the root application component, AppComponent. For smaller applications, this could be the only Angular chapter for the application, as was the case with our sample application prior to this tutorial chapter. As the application gets more features, considered defining a separate Angular chapter for each feature set. For example, a ProductModule, a CustomerModule, and an InvoiceModule. This keeps the code organized, separates the concerns, and prevents the AppModule from getting excessively large and unwieldy. As you add feature chapters, you may find components, pipes, and directives that you want to share across feature chapters. Define one or more shared chapters for these shared pieces. Shared chapters primarily use the exports and declarations arrays, with most of the declared pieces exported as well. If you have a set of services that you want to ensure are loaded when the application is loaded, consider defining a core chapter for those services. Be sure that the core chapter is imported only once in the root application chapter. Since the core chapter is for services, they primarily have providers, none of which are exported. We did not create a core chapter for our sample application since our service needs are limited, but you may find them useful for your applications. And as we discussed in the last clip, we can also refactor our routes into their own routing chapters. When creating an Angular chapter, we build a class and decorate it with the NgModule decorator. The NgModule metadata includes the bootstrap array for defining the list of startup components. In many cases there is only one, the root component of the application. The declarations array declares which components, directives, and pipes belong to this chapter. The exports array identifies the list of components, directives, and pipes that an importing chapter can use. The imports array lists supporting chapters. These chapters provide components, directives, and pipes needed by the components in this chapter. The providers array lists the service providers. Angular registers each provider with Angular's root application injector, so these services are available to be injected into any class in the application. This tutorial chapter was all about Angular chapters. We took a second look at the definition and purpose of an Angular chapter. We then focused in on the Angular chapter metadata and covered the truths to keep in mind when using that metadata. We leveraged that knowledge to create a feature chapter for our application, and took it one step further, defining a shared chapter to reduce duplication in our application. Lastly we re-examined our application root Angular chapter and saw how it orchestrates the application as a whole. If you are building a small application, such as the sample application we've created in this tutorial, you may only need the one root application chapter, as shown here. But as your application grows, you'll want to refactor into feature chapters and shared chapters, like this. into multiple chapters. We have our feature chapter, ProductModule, that encapsulates all of the product features. There will be more feature chapters as our application grows. We have our SharedModule that shares commonly used components, directives, and pipes with any chapter that imports it. Currently we import it into the ProductModule. As we build more feature chapters, we'll import it into them as well. And we have our AppModule that orchestrates the application. Each feature chapter is added to the AppModule's imports array to extend the application with those features. Whew, it's been quite a journey. Now let's circle back to the beginning and spend a little more time with the Angular CLI.