This is the second part of Building an Angular 6 Material CRUD Forms. You will learn how to:
- Part 1 – Create a SPA
- Part 2 – Create a Core Module (This Post)
- Part 3 – Create a Shared Module
- Part 4 – Create a Contacts Module
- Part 5 – Create a Page Not Found Module
The source code for this tutorial series is published on GitHub. Demo application is hosted in Microsoft Azure.
Part 2: Create a Core Module
In the previous tutorial, we created the initial structure of the Single-Page Application (SPA) using Angular 6. In this tutorial, we will use the same technologies to create a Core Module.
Client-Side Technologies:
- Angular 6.0.8
- Angular CLI 6.0.8
- TypeScript 2.7.2
- Angular Material 7.2.1
- In Memory Web Api 0.8.0
- Reactive Extensions Library for JavaScript 6.3.3
The application structure, styles, and patterns follow the recommendations outlined in the official Angular Style Guide.
Task 1. Review the Core Module Structure
-
The
CoreModule
contains the following components and services:123456789101112131415161718192021222324angular-material-crud-forms // Workspace Folder|--src|--core // Core Module|--database // In Memory Service|--in-memory-data.service.ts|--logger // Logger|--logger.service.ts|--main-layout // Layout Component|--main-layout.component.html|--main-layout.component.scss|--main-layout.component.ts|--nav-bar // Navbar Component|--nav-bar.component.html|--nav-bar.component.scss|--nav-bar.component.ts|--toast // Toast Service|--toast.service.ts|--toolbar // Toolbar Component|--toolbar.component.html|--toolbar.component.scss|--toolbar.component.ts|--core.module.ts|--index.ts // Barrel|--module-import-guard.ts // Module Import GuardThe
CoreModule
provides singleton services, global components, and core functionalities. Only the rootAppModule
should import theCoreModule
.
Task 2: Create In Memory Data Service
-
Run the following CLI command to create an
InMemoryDataService
:
ng generate service core/database/in-memory-data
-
Open the service named in-memory-data.service.ts file in the database folder.
-
Replace the code in this file with the following:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212import { Injectable } from '@angular/core';import { InMemoryDbService } from 'angular-in-memory-web-api';@Injectable({// Declare that this service should be created// by the root application injectorprovidedIn: 'root'})export class InMemoryDataService implements InMemoryDbService {// Create a "database" with a set of collection named "contacts"createDb() {const contacts = [{id: 1,firstName: 'Quinn',lastName: 'Nixon',email: 'qnixon@gmail.com',work: 'design',street: '1760 Hillhurst Ave',city: 'Los Angeles',state: 'CA',zip: '90027'},{id: 2,firstName: 'Eric',lastName: 'Smith',email: 'esmith@gmail.com',work: 'code',street: '1910 Pacific Ave',city: 'Dallas',state: 'TX',zip: '75201'},{id: 3,firstName: 'Carlson',lastName: 'Cox',email: 'ccox@gmail.com',work: 'deploy',street: '12345 Beltline Rd',city: 'Dallas',state: 'TX',zip: '75080'},{id: 4,firstName: 'Kelsea',lastName: 'Kelly',email: 'kkelly@gmail.com',work: 'design',street: '33915 Coal Heritage Rd',city: 'Northfork',state: 'WV',zip: '24868'},{id: 5,firstName: 'Aino',lastName: 'Uno',email: 'auno@gmail.com',work: 'code',street: '185 N High St',city: 'Columbus',state: 'OH',zip: '43215'},{id: 6,firstName: 'Amy',lastName: 'Little',email: 'alittle@gmail.com',work: 'deploy',street: '3601 Dallas Pkwy',city: 'Plano',state: 'TX',zip: '75093'},{id: 7,firstName: 'Doris',lastName: 'Chandler',email: 'dchandler@gmail.com',work: 'design',street: '1660 India St',city: 'San Diego',state: 'CA',zip: '92110'},{id: 8,firstName: 'Brielle',lastName: 'Davidson',email: 'bdavidson@gmail.com',work: 'code',street: '5640 Kearny Mesa Rd Ste H',city: 'San Diego',state: 'CA',zip: '92110'},{id: 9,firstName: 'Vivian',lastName: 'Hurst',email: 'vhurst@gmail.com',work: 'deploy',street: '3960 W Point Loma Blvd',city: 'San Diego',state: 'CA',zip: '92110'},{id: 10,firstName: 'Haley',lastName: 'Frost',email: 'hforst@gmail.com',work: 'design',street: '1443 W Fullerton Ave',city: 'Chicago',state: 'IL',zip: '60614'},{id: 11,firstName: 'Fiona',lastName: 'Gaines',email: 'fgaines@gmail.com',work: 'code',street: '1443 W Fullerton Ave',city: 'Chicago',state: 'IL',zip: '60603'},{id: 12,firstName: 'Olivia',lastName: 'Flynn',email: 'oflynn@gmail.com',work: 'deploy',street: '1541 W Bryn Mawr Ave',city: 'Chicago',state: 'IL',zip: '60660'},{id: 13,firstName: 'Jennifer',lastName: 'Marshall',email: 'jmarshall@gmail.com',work: 'design',street: '509 Amsterdam Ave',city: 'New York',state: 'NY',zip: '10024'},{id: 14,firstName: 'Hope',lastName: 'Kennedy',email: 'hkennedy@gmail.com',work: 'code',street: '524 Court St',city: 'Brooklyn',state: 'NY',zip: '11231'},{id: 15,firstName: 'Sandra',lastName: 'Fitzpatrick',email: 'sfitzpatrick@gmail.com',work: 'deploy',street: '565 Gorge Rd',city: 'cliffside Park',state: 'NJ',zip: '07010'},{id: 16,firstName: 'Ashton',lastName: 'Silva',email: 'asilva@gmail.com',work: 'design',street: '4529 Sand Point Way NE',city: 'Seattle',state: 'WA',zip: '98105'},{id: 17,firstName: 'Peter',lastName: 'Byrd',email: 'pbyrd@gmail.com',work: 'deploy',street: '710 8th Ave S',city: 'Seattle',state: 'WA',zip: '98104'}];return { contacts };}}We use In-memory Web API to simulate CRUD data persistence operations without a real server. It intercepts Angular
HTTPClient
requests, redirects them to the in-memory data store –contacts
collection, and returns simulated responses.
Task 3: Create the Logger
-
Run the following CLI command to create a
Logger
:
ng generate service core/logger/logger
-
Open the service named logger.service.ts file in the logger folder.
-
Replace the code in this file with the following:
12345678910111213141516import { Injectable } from '@angular/core';@Injectable({// Declare that this service should be created// by the root application injectorprovidedIn: 'root'})export class Logger {log(msg: string) {console.log(msg);}error(msg: string) {console.error(msg);}}The
Logger
class logs the message to the browser console.
Task 4: Create the Layout Component
-
Run the following CLI command to create a
MainLayoutComponent
:
ng generate component core/main-layout/main-layout --flat --skipImport=true
-
Open the component named main-layout.component.ts file in the main-layout folder.
-
Replace the code in this file with the following:
1234567891011121314151617181920212223import { Component, Input, ViewChild } from '@angular/core';import { MatSidenav } from '@angular/material';@Component({selector: 'app-main-layout',templateUrl: './main-layout.component.html',styleUrls: ['./main-layout.component.scss']})export class MainLayoutComponent {// Receive menu object and title from the parent 'AppComponent'@Input() title: string;@Input() menu: Array<Object>;@ViewChild('sidenav') sidenav: MatSidenav;constructor() { }toggleSidenav() {// trigger the child component's 'toggle' methodthis.sidenav.toggle();}}The
MainLayoutComponent
can toggle the#sidenav
menu. -
Open the view named main-layout.component.html file in the main-layout folder.
-
Replace the code in this file with the following:
12345678910111213141516171819202122<!-- Toolbar includes logo, titles, and actions --><app-toolbar [title]="title" [menu]="menu" (toggleSidebar)="toggleSidenav()"></app-toolbar><!-- End Toolbar --><mat-sidenav-container fullscreen><!-- Collapsible side content --><mat-sidenav #sidenav [mode]="'over'" class="navbar"><app-navbar [menu]="menu"></app-navbar></mat-sidenav><!-- End Collapsible side content --><!-- Main Content Area --><div class="main-content"><div class="mat-app-background"><!-- Routed view --><router-outlet></router-outlet></div></div><!-- End Main Content Area --></mat-sidenav-container>The main-layout.component.html file contains the master layout for our HTML. It provides a
shell
with three regions: a toolbar area, a collapsible side content, and a main content area. The toolbar area renders theToolbarComponent
‘s view between theapp-toolbar
tags. The collapsible sidebar area renders theNavbarComponent
‘s view between theapp-navbar
tags. The main content area usesrouter-outlet
directive to display the views produced by the Router. In other words, when you click a navigation link, it’s corresponding view is loaded in the main content area. -
The following highlighted code uses an Angular’s property binding to pass data from the parent
MainLayoutComponent
to the child component:- Line 2 uses
[title]="title"
to pass the value of title and[menu]="menu"
to pass menu object to the childToolbarComponent
. - Line 8 uses
[menu]="menu"
to pass menu object to the childNavbarComponent
.
- Line 2 uses
-
Line 2 uses an Angular’s event binding to bind the
(toggleSidebar)
event totoggleSidenav()
. -
Open the styles named main-layout.component.scss file in the main-layout folder.
-
Replace the code in this file with the following:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// @import "./../../../styles/settings/breakpoints.scss";// @import "./../../../styles/settings/layout.scss";// @import "./../../../styles/settings/colors.scss";@import "breakpoints";@import "layout";@import "custom-colors";.main-content {padding: {top: 0;left: 15px;right: 15px;bottom: 0;}@include breakpoint($narrow-devices) {padding: {left: 15px;right: 15px;}}height: 100%;overflow: auto;}.navbar {min-width: 300px;max-width: 300px;}:host /deep/.mat-sidenav-container[fullscreen] {top: 55px;@include breakpoint($narrow-devices) {top: 64px;}}// See https://github.com/angular/material2/issues/998:host /deep/ .mat-sidenav-content {transform: none !important;}.main-content {& /deep/ .outlet,& /deep/ .maxed-width {@include breakpoint($narrow-devices) {max-width: $page-max-width;margin: {left: auto;right: auto;}}}}Setting the
stylePreprocessorOptions
withincludePaths
option in the angular.json file has shorten the@import
statements in Lines 4 – 6.
Task 5: Create the Navbar Component
-
Run the following CLI command to create a
NavbarComponent
:
ng generate component core/navbar/navbar --flat --skipImport=true
-
Open the component named navbar.component.ts file in the navbar folder.
-
Replace the code in this file with the following:
1234567891011121314151617import { ChangeDetectionStrategy, Component, OnInit, Input } from '@angular/core';@Component({selector: 'app-navbar',templateUrl: './navbar.component.html',styleUrls: ['./navbar.component.scss'],changeDetection: ChangeDetectionStrategy.OnPush})export class NavbarComponent implements OnInit {// Receive menu data from the parent 'MainLayoutComponent'@Input() menu: Array<Object>;constructor() { }ngOnInit() {}} -
Open the view named navbar.component.html file in the navbar folder.
-
Replace the code in this file with the following:
1234567<mat-list class="sidenav-menu"><mat-list-item *ngFor="let menu_item of menu; let i = index"><a class="sidenav-menu-item-static" mat-button [href]="[menu_item.url]" target="_blank" rel="noopener"><span>{{ menu_item.name }}</span></a></mat-list-item></mat-list>We are using the *ngFor directive to iterate through the array of
menu
and display the menu items.
Task 6: Create the Toast Service
-
Run the following CLI command to create a
ToastService
:
ng generate service core/toast/toast
-
Open the service named toast.service.ts file in the toast folder.
-
Replace the code in this file with the following:
123456789101112131415import { Injectable } from '@angular/core';import { MatSnackBar } from '@angular/material';@Injectable({providedIn: 'root'})export class ToastService {constructor(public snackBar: MatSnackBar) { }openSnackBar(message: string, action: string) {this.snackBar.open(message, action, {duration: 2000});}}The
ToastService
opens a snack-bar that contains a simple message with an action for 2 seconds.
Task 7: Create the Toolbar Component
-
Run the following CLI command to create a
ToolbarComponent
:
ng generate component core/toolbar/toolbar --flat --skipImport=true
-
Open the component named toolbar.component.ts file in the toolbar folder.
-
Replace the code in this file with the following:
12345678910111213141516171819202122232425262728import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';@Component({selector: 'app-toolbar',templateUrl: './toolbar.component.html',styleUrls: ['./toolbar.component.scss'],changeDetection: ChangeDetectionStrategy.OnPush})export class ToolbarComponent implements OnInit {// Receive menu object and title from the parent 'MainLayoutComponent'@Input() menu: Array<Object>;@Input() title: string;// Raise the event to the parent 'MainLayoutComponent'@Output() toggleSidebar = new EventEmitter();sidebarOpened = false;ngOnInit() {}/*** Toggle the sidenav menu.*/toggleSidenav() {this.sidebarOpened = !this.sidebarOpened;this.toggleSidebar.emit(this.sidebarOpened);}} -
Open the view named toolbar.component.html file in the toolbar folder.
-
Replace the code in this file with the following:
123456789101112131415<mat-toolbar class="header-toolbar mat-elevation-z6"><button class="sidenav-toggle" aria-label="Toggle Sidebar" (click)="toggleSidenav()"><mat-icon class="menu-icon" aria-label="Side nav toggle icon">menu</mat-icon></button><div class="all-menu"><a [routerLink]="''"><img alt="angular" class="docs-angular-logo" src="../../../assets/img/angular-white-transparent.svg"><span class="app-title">Material CRUD Forms</span></a></div><div class="desktop-menu"><span class="spacer"></span><a *ngFor="let menu_item of menu; let i = index" [href]="[menu_item.url]" target="_blank" rel="noopener" mat-button class="menu-item">{{ menu_item.name }}</a></div></mat-toolbar>Clicking the menu icon in the top left corner of the toolbar, a click event calls the
toggleSidenav()
method in turn to tellMainLayoutComponent
to toggle the#sidenav
menu. -
Open the style named toolbar.component.scss file in the toolbar folder.
-
Replace the code in this file with the following:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980// @import "./../../../styles/settings/breakpoints.scss";// @import "./../../../styles/settings/colors.scss";@import "breakpoints";@import "custom-colors";:host {z-index: 1;position: fixed;top: 0;width: 100%;}.app-title {font-size: 13px;//text-transform: uppercase;@include breakpoint($narrow-devices) {display: none;}}.sidenav-toggle {@include breakpoint($narrow-devices) {display: none;}}.all-menu {.app-title {display: inline-block;color: $white;}}.desktop-menu {display: none;@include breakpoint($narrow-devices) {display: flex;width: 100%;}}.github-logo {img {margin-right: 10px;}}.sidenav-toggle {cursor: pointer;margin-right: 10px;color: $white;&:focus,&:hover,&:active {outline: none;}}.menu-item {text-transform: uppercase;font-size: 13px;}.spacer {flex: 1 1 auto;}.header-toolbar {//background-color: $header-color;background-color: $dark-color;color: $white;box-shadow: 0 4px 20px 0 rgba(0,0,0,.14), 0 7px 12px -5px rgba(255,152,0,.46);}.docs-angular-logo {height: 26px;margin: 0 4px 3px 0;vertical-align: middle;}Setting the
stylePreprocessorOptions
withincludePaths
option in the angular.json file has shorten the@import
statements in Lines 3 – 4.
Task 8: Create the Import Guard Function
-
Add a file named module-import-guard.ts in the core folder.
-
Replace the code in this file with the following:
12345export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {if (parentModule) {throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);}}Only the root
AppModule
should import theCoreModule
. This functionthrowIfAlreadyLoaded
guards against reimporting of theCoreModule
.
Task 9. Create the Core Module
-
Run the following CLI command to create a
CoreModule
:
ng generate module core
-
Open the module named core.module.ts file in the core folder.
-
Replace the code in this file with the following:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import { NgModule, Optional, SkipSelf } from '@angular/core';import { CommonModule } from '@angular/common';import { HttpClientModule } from '@angular/common/http';import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'import { RouterModule } from '@angular/router';import { MaterialModule } from '@app/material/material.module';import { InMemoryDataService } from './database/in-memory-data.service';import { Logger } from './logger/logger.service';import { MainLayoutComponent } from './main-layout/main-layout.component';import { NavbarComponent } from './navbar/navbar.component';import { throwIfAlreadyLoaded } from './module-import-guard';import { ToastService } from './toast/toast.service';import { ToolbarComponent } from './toolbar/toolbar.component';@NgModule({imports: [CommonModule,MaterialModule,HttpClientModule,// The HttpClientInMemoryWebApiModule module intercepts HTTP requests// and returns simulated server responses.// Remove it when a real server is ready to receive requests.HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { dataEncapsulation: false }),RouterModule],declarations: [MainLayoutComponent,NavbarComponent,ToolbarComponent],exports: [MainLayoutComponent],providers: [Logger,ToastService]})export class CoreModule {/* make sure CoreModule is imported only by one NgModule the AppModule */constructor(@Optional() @SkipSelf() parentModule: CoreModule) {throwIfAlreadyLoaded(parentModule, 'CoreModule');}} -
Lines 19 – 28 import a list of modules (
CommonModule
,MaterialModule
,HttpClientModule
,HttpClientInMemoryWebApiModule
,RouterModule
) whose exported components, directives, or pipes are referenced by the components declared in theCoreModule
. -
Lines 29 – 33 declare a list of components (
MainLayoutComponent
,NavbarComponent
,ToolbarComponent
) that belong to theCoreModule
. -
Lines 34 – 36 export
MainLayoutComponent
public so that other module’s component templates can use it. -
Lines 37 – 40 register a list of services (
Logger
,ToastService
) so that the same instance of these services are available to all parts of the application. -
Line 43 exports the
CoreModule
so that the AppModule can import it.
Task 10: Create the Barrel
-
Add the barrel named index.ts file to the core folder.
-
Replace the code in the file with the following:
123export { MainLayoutComponent } from './main-layout/main-layout.component';export { Logger } from './logger/logger.service';export { ToastService } from './toast/toast.service';The index.ts re-exports
MainLayoutComponent
,Logger
andToastService
ofCoreModule
to simplify the @import statement of these entities.
Task 11: Update the App Module
-
Open the named app-module.ts file in the root folder.
-
Replace the code in the file with the following:
12345678910111213141516171819202122232425import { BrowserModule } from '@angular/platform-browser';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';import { NgModule } from '@angular/core';import { AppComponent } from './app.component';import { AppRoutingModule } from './app-routing.module';import { CoreModule } from './core/core.module';@NgModule({imports: [BrowserModule,BrowserAnimationsModule,AppRoutingModule,CoreModule],declarations: [AppComponent],providers: [],bootstrap: [AppComponent]})export class AppModule { } -
Lines 10 – 15 import the following supporting modules whose exported components, directives, or pipes are referenced by the component declared in the
AppModule
.BrowserModule
: provides critical services that are essential to launch and run our app in the browserBrowserAnimationsModule
: provides the animation capabilitiesAppRoutingModule
: is arouting module
which provides the application-wide configured services with routes in the root moduleCoreModule
: provides singleton services, global components, and core functionalities
-
Lines 16 – 18 declare
AppComponent
that belongs to theAppModule
. -
Lines 20 – 22 bootstrap the root component named
AppComponent
when Angular starts the application. -
Line 25 exports the
AppModule
so that the main.ts file can import it.
Task 12. Build and Serve the Application
-
Run the following CLI command to start the server, launch the application, and automatically open your browser to
http://localhost:4200
:
ng serve -o
-
If there are errors, fix them before moving on to the next tutorial. Otherwise, you should still see the same screen as follows:
-
Press the following keys to stop the server (on Mac):
control + c
In the next tutorial, we will build the Shared Module using Angular 6.
The source code for this tutorial series is published on GitHub. Demo application is hosted in Microsoft Azure.
References
- QuickStart
- The official Angular Style Guide
- Angular’s Routing & Navigation
- Angular’s Lazy Loading Feature Modules
- CLI Command Reference
- 6 Best Practices & Pro Tips when using Angular CLI by Tomas Trajan
- Barrel files: to use or not to use ? by Adrian Fâciu
- A Comprehensive Guide to Angular onPush Change Detection Strategy by Netanel Basal
Leave a Reply