Ng101
This project was generated with Angular CLI version 11.2.7.
Log
How this workspace was generated
npx @angular/cli new ng101 --directory=angular-101 --package-manager=yarn --strict --create-application=false
How demo app was generated
yarn ng generate application demo --routing --style=scss
What demo app initially looked like
yarn start --open
How Angular Material was added
yarn ng add @angular/material
How unit tests are run
yarn test
Note: Karma was configured to run on headless Chrome. The default setup would open a new Chrome instance.
How lazy-loaded module was added
yarn ng g module todo-list --module=app --route=todo-list
Note: g
is the short alias for generate
.
How to generate a layout
# first create the module
yarn ng g module layouts/main-layout --module=todo-list/todo-list
# then create the component
yarn ng g component layouts/main-layout/main-layout --export --change-detection=OnPush --flat
Note 1: Configured todo list routing to have nested routes.
Note 2: Obviously, some styles as well as Material components were used to get that result.
How to customize Material theme
@import "~@angular/material/theming";
$app-primary: mat-palette($mat-blue-grey, 900);
$app-accent: mat-palette($mat-yellow, 700);
$app-warn: mat-palette($mat-red, 600);
$app-theme: mat-light-theme(
(
color: (
primary: $app-primary,
accent: $app-accent,
warn: $app-warn,
),
)
);
Note 1: Material has over 900 free icons and, when required, custom icons can be registered via a service.
Note 2: Angular protects us against XSS attacks. Custom SVG source could be registered only after explicit trust was granted.
How linter is run
yarn lint
Note: TS Lint is deprecated and is expected to be replaced by Angular team.
How mock library was generated
Important: This step is not needed when developing Angular apps with a backend.
yarn ng g library mock --entry-file=index --skip-package-json
How MSW and PouchDB were integrated
Important: This step is not needed when developing Angular apps with a backend.
- Installed dependencies.
- Added pouchdb to script injected by Angular when app is built.
- Used MSW CLI to create mockServiceWorker.js and added the generated file to assets.
- Created models and request handlers.
- Initiated msw only in development.
Note: Using MSW is a personal preference. There are other mocking options such as Angular in-memory-web-api or providing mock services with dependency injection.
How AsyncPipe, ngIf, nfFor, and ngClass works
<mat-card>
<mat-selection-list *ngIf="list$ | async as list; else spinner">
<mat-list-option [selected]="todo.done" *ngFor="let todo of list.rows">
<span [ngClass]="{ done: todo.done }">{{ todo.title }}</span>
</mat-list-option>
</mat-selection-list>
<ng-template #spinner>
<div class="spinner">
<mat-spinner diameter="60" color="accent"></mat-spinner>
</div>
</ng-template>
</mat-card>
How to execute async operations before initialization
The mock DB implementation so far has an error. When site data is cleared and the page is refreshed, the first response is empty.
This is due to lack of proper asynchronous initialization. APP_INITIALIZER
serves that purpose.
{
provide: APP_INITIALIZER,
useFactory: () => {
return async () => {
await seedDb();
worker.start();
};
},
multi: true,
}
How to update a record
CRUD operations via AJAX are probably the most common implementations in web development. Angular has an awesome HttpClient
to do all sorts of HTTP requests.
@Injectable()
export class TodoService {
constructor(private http: HttpClient) {}
update(id: string, input: TodoUpdate) {
return this.http.put<void>(`/api/todos/${id}`, input);
}
}
...and in component class...
@Component(/* removed for brevity */)
export class TodoListComponent {
private listUpdate$ = new Subject<void>();
list$ = merge(of(0), this.listUpdate$).pipe(
switchMap(() => this.todo.getList())
);
constructor(private todo: TodoService, private dialog: MatDialog) {}
toggleDone(todo: Rec<Todo>) {
this.todo
.update(todo.id, { title: todo.title, done: !todo.done })
.subscribe(() => this.listUpdate$.next());
}
}
Note: Did you notice the canceled request? This is due to use of switchMap
in list$
.
How to delete a record
Sometimes, we need to get some confirmation before proceeding with the request. HttpClient
uses RxJS observables, so that usually is quite easy.
@Component(/* removed for brevity */)
export class TodoListComponent {
@ViewChild("deleteDialog") deleteDialog?: TemplateRef<any>;
/* removed for brevity */
deleteRecord(todo: Rec<Todo>) {
this.dialog
.open(this.deleteDialog!, { data: todo.title })
.afterClosed()
.pipe(
concatMap((confirmed) =>
confirmed ? this.todo.delete(todo.id) : EMPTY
)
)
.subscribe(() => this.listUpdate$.next());
}
}
How to refactor routes
Angular modules manage their own child routes and parent modules are unaware of grand child routes. This makes it easy to refactor routes.
@NgModule({
imports: [
RouterModule.forChild([{ path: "", component: TodoListComponent }]),
],
exports: [RouterModule],
})
export class TodoListRoutingModule {}
@NgModule({
imports: [
RouterModule.forChild([
{
path: "",
component: MainLayoutComponent,
children: [
{ path: "", pathMatch: "full", loadChildren: () => TodoListModule },
],
},
]),
],
exports: [RouterModule],
})
export class TodosRoutingModule {}
@NgModule({
imports: [
RouterModule.forRoot([
{
path: "",
loadChildren: () =>
import("./todos/todos.module").then((m) => m.TodosModule),
},
]),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
Although there is now an additional module between, nothing changes.
How to create a new record
yarn ng g module todo-create --module=todos/todos --route=create
@Component(/* removed for brevity */)
export class TodoCreateComponent {
form!: FormGroup;
constructor(
private fb: FormBuilder,
private router: Router,
private todo: TodoService
) {
this.buildForm();
}
goToListView() {
this.router.navigate([".."]);
}
submitForm() {
if (!this.form.valid) return;
this.todo.create(this.form.value).subscribe(() => this.goToListView());
}
private buildForm(): void {
this.form = this.fb.group({
title: [null, Validators.required],
});
}
}
...and in template...
<!-- removed for brevity -->
<form [formGroup]="form" id="todo-form" (ngSubmit)="submitForm()">
<mat-form-field>
<mat-label>Todo title *</mat-label>
<input
matInput
formControlName="title"
placeholder="Become a Jedi Knight"
maxlength="256"
autocomplete="off"
#title
/>
<mat-hint align="end">{{ title.value.length }} / 256</mat-hint>
<mat-error *ngIf="form.get('title')?.invalid">
Sorry, this field is <strong>required</strong>.
</mat-error>
</mat-form-field>
</form>
<!-- removed for brevity -->
How HTTP errors were handled
yarn ng g interceptor common/error --flat
...then...
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private snackBar: MatSnackBar) {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError(({ error, status }) => {
this.snackBar.open(`${status}: ${error}`, "HTTP Error", {
duration: 3000,
});
return EMPTY;
})
);
}
}
...and in root module...
@NgModule({
/* removed for brevity */
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true,
},
],
})
export class AppModule {}
Note: Http interceptors can intercept outgoing requests as well.
How to get a production build of the app
yarn build --prod
Further help
To get more help on the Angular CLI use ng help
or go check out the Angular CLI Overview and Command Reference page.