Angular 6 by Example Notes
These are my (incomplete) notes about Angular 6 by Example. It starts from the very basics, but eventually covers most of the required tools needed for professional Angular development.
It’s intended to be read from start to end, as the sample application is built in an iterative fashion as the book progresses.
The full book is available at Safari Online.
Summary of Angular
Angular’s way of life:
- create components
- bundle them into modules
- bootstrap your application
- ????
- PROFIT!
Angular itself is built as a set of modules that we import into our application.
What’s a component anyways?
The component pattern is simply about combining smaller, discrete building blocks into larger finished products. Components have clearly defined interfaces and they themselves are composed of other components. It’s turtles all the way down.
The component pattern isn’t specific to Angular, or even specific to software development.
Examples:
- DAP: microSD, audio jack, battery
- Keyboard: switches, keycaps, USB connector
- Computer: hard drive, CPU, RAM sticks, PSU
Extensive usage of this pattern in Angular 2 and greater was enabled by Web Components, and the release of ES6 (2015).
Web components:
- Custom elements
- Shadow DOM
- Templates
- HTML imports (actually not used by Angular, as it relies on JavaScript module loading.)
It’s possible to use ES5 with Angular, but most of the time what’s used is either ES6 (2015) or TypeScript instead.
TypeScript is a superset of ES6 which adds Types and Decorators.
Component-based design
- look at the UI and expected behavior (features)
- encapsulate all of this into a component
- separate the UI into a view
- separate the behavior into a class
Order of things:
Functionality -> Data & behavior -> UI
Barrel modules
import { Component } from '@angular/core';
Barrels are a convenience feature that consist of modules whose only task is to re-export other modules for convenient import.
- https://angular.io/guide/glossary#barrel
- https://medium.com/@adrianfaciu/barrel-files-to-use-or-not-to-use-75521cd18e65
- https://www.reddit.com/r/Angular2/comments/6fqzxc/modules_vs_barrels/
Decorators
Decorators are identified by being prefixed with the “@” symbol. The component decorator also has a property called template, which identifies the HTML markup. We could have a templateUrl property instead.
Module files
Every Angular component must be contained within an Angular module. This means that at the very least we must add one Angular module file to the root of our application. This is called the root module
declarations = An array of the components to be used in our application. imports = importing other modules, for example BrowserModule to run the thing in a browser. providers = used to rgister provides (e.g. services and other objects) that will be available to be used through our application via Dependency Injection. bootstrap = this indicates the first component that will be loaded when our application starts up.
Bootstraping
https://angular.io/guide/bootstrapping
AppComponent acts as a blueprint for the component, but its code doesn’t run until something else runs it. This is the job for the main.ts bootstraper.
main.ts -> AppModule -> AppComponent
main.ts
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
Angular Symbols
<input type="number" [value]="guess" (input)="guess = $event.target.value" />
In the Angular world, these symbols mean the following:
{{ and }} are interpolation symbols
[ ] represents property bindings
( ) represents event bindings
“?.” is the safe navigation operator. It works like in C#, so no surprise here. It’s often useful for async operations where data isn’t available immediately, and where it’s preferable to show nothing while it’s loaded instead of just crashing the application with no survivors.
Structural directives
<div>
<p *ngIf="deviation<0" class="alert alert-warning"> Your guess is higher.</p>
<p *ngIf="deviation>0" class="alert alert-warning"> Your guess is lower.</p>
<p *ngIf="deviation===0" class="alert alert-success"> Yes! That's it.</p>
</div>
Structural directives allow us to manipulate the structure of DOM elements. It’s a
shorthand for ng-template
, which is Angular implementation of the Web Components
we discussed earlier.
One-way data binding
Angular does one-way data binding; copypasting from the book:
- Update components and domain objects
- Run change detection
- Re-render elements in the view that have changed.
Every browser event and some other async events like (XHR requests) trigger this process.
More details: https://vsavkin.com/two-phases-of-angular-2-applications-fda2517604be#.fabhc0ynb
More resources:
- https://angular.io/docs = official documentation
- https://blog.angular.io/ = official blog
- https://github.com/gdi2290/awesome-angular = awesome-angular meme list
- http://angularexpo.com/ = gallery of applications made with angular. Some even include Source Code.
// END OF CHAPTER 1
The example app in the book is called 7 minute workout example app, and it’s available on its Github repository
7 minute workout: set of 12 exercises in quick succession within 7 minute time span 12 exercises, 30 seconds for each of the exercises with 10 second rests.
Angular CLI
ng new PROJECT_NAME
ng serve --open
// serve and open in browser.
"projects": {
"project-name": {
...
"architect": {
"serve": {
"options": {
"host": "foo.bar",
"port": 80 // CAMBIAR PUERTO POR DEFECTO
}
}
}
...
}
}
What comes with the CLI anyways?
- A build system based on webpack
- A scaffolding tool to generate standard Angular artifacts (modules, etc.)
- They conform to the Angular Style Guide (http://bit.ly/ngbe-styleguide)
- A linter (codelyzer - http://bit.ly/ngbe-codelyzer).
- Preconfigured unit and e2e test frameworks.
About the directory structure generated
- “e2e” directory for end to end tests.
- “src” is where the dev magic happens.
- “assets” is for static content
- “app” is the app’s source code
- “environments” is for environment configurations (e.g. qa, prod, etc.)
Using feature folders is recommended inside app. Files are grouped according to features, and as a feature grows too big, it gets divided into smaller “features”.
Creating new modules
ng generate module workout-runner –module app.module.ts
- Create a new WorkoutRunnerModule inside a workout-runner directory.
- Imports the module into app.module.ts
This is our new feature module, which by default looks like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [
CommonModule
],
declarations: []
})
export class WorkoutRunnerModule { }
CommonModule has common Angular directives such as ngIf and ngFor, allowing us to use these directives across any component/directive defined in WorkoutRunnerModule.
More About bootstrapping
The Angular CLI does the heavy lifting of compiling our application and including the resulting script into index.html, reason why it’s not included in the source index.html.
These behaviors are part of the default Angular CLI configuration (.angular-cli.json).
Angular Modules
Angular modules are a way to organize code into chunks that belong together in some way, for example, belonging to the same “feature”.
A module primarily defined:
- components/directives/pipes it owns
- components/directives/pipes it makes public for other modules
- other modules that it depends on
- services that the module wants to make available application-wide
Angular itself is also divided into modules. e.g.:
CommonModule
contains standard framework thingiesRouterModule
is needed if the want to use the Angular routing framework.HttpModule
for HTTP server communication.
Creating an Angular module
Add the @NgModule decorator to a TypeScript class. That’s it.
The decorator has multiple attributes that allows us to define several things:
- External dependencies using
imports
. - Module artifacts using
declarations
. - Module exports using
exports
. - Services defined inside the module that need to be registered globally using
providers
. - ONLY FOR THE ROOT MODULE: the root component, using
bootstrap
.
More about Angular Modules:
- https://angular.io/guide/ngmodules
- https://angular.io/guide/ngmodule-faq
- https://stackoverflow.com/questions/39062930/what-is-difference-between-declarations-providers-and-import-in-ngmodule
- https://blog.angularindepth.com/avoiding-common-confusions-with-modules-in-angular-ada070e6891f
- https://blog.ng-book.com/introduction-to-ngmodules-components-dependency-injection-and-testing/
Imports, declarations, exports and providers
Already “summarized” two times, but those are VERY IMPORTANT concepts as they’re part of Angular Modules, and Angular Modules are literally in any Angular project.
What declarable is and what’s not part of a module is defined by the declarations property of the @NgModule decorator. A declarable is either a component, a directive, or a pipe. A declarable must be declared in EXACTLY ONE (1) module in an Angular application.
Declared declarables are available to any other declarable in the same module by default. AKA. they’re private.
A module can make some of its declarations “public” by exporting them via the exports array.
A module can import the exported declarations of other module by importing them, listing the modules to import in the imports array. Non-exported declarations will not be available in the importer module. DO NOT RE-DECLARE IMPORTED DECLARATIONS.
Providers make services available at the global level. A module that contains only services is often imported once at the root module. Since Angular 6, providers is only used for overriding existing services. TODO: clarificar.
Regarding providers in Angular 6, the preferred way is to use DI, though: https://itnext.io/understanding-provider-scope-in-angular-4c2589de5bc
From src/app: ng generate component workout-runner -is
- Creates .html file for the component’s template
- creates .ts file for the component’s logic
- creates a .spec.ts file for testing the component
- Updates the module’s file
** The -is flag means “Inline Styles”, which prevents the creation of a separate style file, because in this example we’re using global styles.
When creating a new component, consider using a custom prefix to our component selector.
"schematics": {
"@schematics/angular:component": {
"prefix": "abe",
"styleext": "css"
},
"@schematics/angular:directive": {
"prefix": "abe"
}
}
ngOnInit is a function that gets called when a component is initialized.
Component lifecycle hooks
- ngOnInit (defined in OnInit)
- ngOnChanges
- ngOnDestroy
Among others… https://angular.io/guide/lifecycle-hooks
ngOnInit
Fired the 1st time the component’s date bound props are initialized.
Avoid using the class constructor for anything more complex than member init.
Angular Binding Infrastructure
- Interpolation
- Property Binding
- Directives
- Attribute Binding
- Class binding
- Style binding
- Event binding
Interpolation
{{ someExpression }}
The expression can be a function call too, but the function must be pure.
Interpolation as some syntactic sugar over property binding.
<h3>Main heading - {{heading}}</h3>
<h3 [text-content]="' Main heading - '+ heading"></h3>
<img [src]="'/assets/images/' + currentExercise.exercise.image" />
<img src="/assets/images/{{currentExercise.exercise.image}}" />
Property binding
[propertyName]
Usable to set DOM object properties. IT DOESN’T SET THE HTML ATTRIBUTES. The same [] syntax is used for directives.
It can also be used to set Angular components’ input properties, and do some thing with output properties too TODO: specify.
<workout-runner [exerciseDuration]="restDuration"></workout-runner>
Target location
[target]
“target” will be searched among registered known directives, and if it’s not found it will be looked among the DOM object properties. If it isn’t found in either, an error will be raised.
Class binding
[class.class-name]="expression"
If expression is true, the class-name is applied.
Style binding
[style.style-name]="expression"
The result of expression is applied to style-name.
Attribute binding
[attr.aria-value]="expression"
The result of expression is assigned to aria-value attribute (in this example)
Directives
“Directives” is an umbrella term used for component directives, attribute directives, and structural directives. But as Component Directives are refered to as just “components”, “directives” is often used to refer to the other two.
Directives either enhance the appearance/behavior of existing elements/components in an existing view (Attribute Directives), or change the DOM layout of the elements on which they’re applied.
Attribute directives
As said, attribute directives modify the appearance of behavior of existing elements or components.
Some built-in, common attribute directives in Angular are:
ngStyle
ngClass
ngValue
ngModel
ngSelectOptions
ngControl
ngFormControl
ngStyle and ngClass are often used, and they’re an alternative to style and class bindings, as they allow to set multiple style/classes at the same time from a single object.
Structural directives
Official definition:
“Instead of defining and controlling a view like a Component Directive, or modifying the appearance and behavior of an element like an Attribute Directive, the Structural Directive manipulates the layout by adding and removing entire element sub-trees.”
Basically, as already said, structural directives change the DOM itself.
They’re recognizable by being prefixed by an asterisk, e.g.: *ngFor
The asterisk actually extends from this:
<div *ngFor="let video of safeVideoUrls">
<iframe width="198" height="132" [src]="video" frameborder="0" allowfullscreen></iframe>
</div>
To this:
<ng-template ngFor let-video [ngForOf]="safeVideoUrls">
<div>
<iframe width="198" height="132" [src]="video" ...></iframe>
</div>
</ng-template>
So while technically we could write structural directives without a prefixing asterisk, in practice pretty much everyone uses the shorter asterisk syntax.
Now, the string assigned to the ngFor directive isn’t a typical Angular expression, but rather a Angular specific microsyntax that the Angular engine can parse.
https://angular.io/guide/structural-directives#microsyntax
THE -i in
ng generate component video-player -is
DOES NUFFIN!
ngOnChanges() {
this.safeVideoUrls = this.videos ?
this.videos.map(v => this.sanitizer.bypassSecurityTrustResourceUrl(this.youtubeUrlPrefix + v))
: this.videos;
}
Defined in the OnChanges interface. A lifecycle hook that is called when any data-bound property of a directive changes. i.e. whenever the component’s input(s) change.
Adding the @Input decorator to a component property marks it as available for data binding.
@Input() videos: Array<string>;
Angular Security
Angular automatically sanitizes content for us to prevent common attacks, like XSS or undesired 3rd party content embedded in iframes.
The framework defines four security contexts around content sanitization:
- HTML content sanitization, when HTML content is bound using the innerHTML property
- Style sanitization, when binding CSS into the style property
- URL sanitization, when URLs are used with tags such as anchor and img
- Resource sanitization, when using Iframes or script tags; in this case, content cannot be sanitized and hence it is blocked by default
When we know some content is safe, we must explicitly tell Angular to bypass the security checks by using DomSanitizer, which we can import from “@angular/platform-browser”.
constructor(private sanitizer: DomSanitizer) { }
ngOnChanges() {
this.safeVideoUrls = this.videos ?
this.videos.map(v => this.sanitizer.bypassSecurityTrustResourceUrl(this.youtubeUrlPrefix + v))
: this.videos;
}
There are five methods related to security bypass:
- bypassSecurityTrustHtml
- bypassSecurityTrustScript
- bypassSecurityTrustStyle
- bypassSecurityTrustUrl
- bypassSecurityTrustResourceUrl
More details can be found: https://angular.io/guide/security
Outputting HTML shortcut
If you use interpolation and output HTML, it’ll get “escaped”. Greater and lower than symbols will get converted to HTML entities, and so on.
If all what we need is to output unescaped HTML, the easiest thing to do is to property bind to innerHTML as follows:
<div class="card-text" [innerHTML]="steps"></div>
Any other “suspicious” content will get the default treatment though (e.g. script tags will not be rendered)
Pipes
{{ expression | pipeName:param1:param2 | otherPipe }}
Pipes allow us to apply transformations on the output of some expression, often to change how something is displayed to the user.
Commonly used pipes are:
- date
- uppercase
- lowercase
- decimal
- percent
- currency
- json
- slice
Some pipes are dependant on the locale of the user, date for example.
More information about built-in pipes: https://angular.io/api?type=pipe
Custom pipe
ng generate pipe seconds-to-time
The CLI command creates a typescript class with the Pipe definition and its test file in the current directory.
What makes a pipe a pipe are two things:
- @Pipe({ name: ‘secondsToTime’ })
- transform(value: number): any (implementation of the PipeTransform interface)
The first parameter “value” is what’s passed from the previous expression, any further optional or required parameters are added to the transform method and then are passed in the view by separathing them with colons “:”.
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
transform(value: number, exponent: string): number { // exponent
let exp = parseFloat(exponent);
return Math.pow(value, isNaN(exp) ? 1 : exp);
}
}
<p>Super power boost: {{2 | exponentialStrength: 10}}</p>
Conditional display of something
All of the following Angular directives create or destroy the DOM elements, recursively, meaning that components are recreated and data binding is setup again.
This could be potentially expensive, but more importantly, it means that components lose their state. TODO: verify whether this is 100% correct.
If this is not desired, then using a style binding for display like the following would be enough:
[style.display]="isAdmin ? 'block' : 'none'"
ngIf
*ngIf="boolean expression"
Or with an else block:
<div *ngIf="show; else elseBlock">asdf</div>
<ng-template #elseBlock>asdf else</ng-template>
ngSwitch
<div id="parent" [ngSwitch] ="userType">
<div *ngSwitchCase="'admin'">I am the Admin!</div>
<div *ngSwitchCase="'powerUser'">I am the Power User!</div>
<div *ngSwitchDefault>I am a normal user!</div>
</div>
Event binding “globally”
<div (window:keyup)="onKeyPressed($event)"></div>
By default, events only fire-up when the element that declares the event binding is focused. It’s possible to bind events globally to the “window” or “document” by using the syntax already shown.
Angular Event Binding Infrastructure
on-click == (click)
Many many DOM events are supported in Angular, and custom component events can be created too.
Events bubble up when attached to standard HTML elements.
Angular creates a $event object whose content varies depending on the type of event. $event.target contains the original source of an event
Custom events created on Angular components do not support even bubbling.
Event bubbling stops if the expression assigned to the target event evaluates to anything falsey, like void or… false.
Two-way databinding
<input [(ngModel)]="workout.name">
// END OF CHAPTER 2
=====
Routing Infrastructure
Routing is the key piece for making Angular SPAs. It’s composed by the following:
- The Router (Router): The primary infrastructure piece that actually provides component navigation
- The Routing configuration (Route): The component router is dependent upon the routing configuration for setting up routes
- The RouterOutlet component: The RouterOutlet component is the placeholder container (host) where route-specific views are loaded
- The RouterLink directive: This generates hyperlinks that can be embedded in the anchor tags for navigation
During project creation with Angular CLI, the router can be added automatically.
We can check we have the required dependency by looking in package.json for “@angular/router”.
The Router
It’s there to:
- Enable navigation between components on route change.
- Pass routing data between component views.
- Make the state of the current route available to active/loaded components.
- Provide APIs that allow navigation from component code.
- Track the navigation history, allowing us to use browser history buttons.
- Provide life cycle events and guard conditions that allow us to affect navigation based on some external factors.
For starters, add
Note: the router is a framework service, and in Angular any class, object, function that provides some generic functionality is called a service. There’s no special decorator or the like to declare services, as it’s the case for components/directives/pipes. This makes it simpler when compared to AngularJS’ service, factory, provider, value, constant.
Best practices: https://angular.io/guide/styleguide#style-04-06
Route configuration
We can generate routing by following the instructions at https://angular.io/guide/router#integrate-routing-with-your-app
The following are instructions for a manual setup, according to the book:
app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WorkoutRunnerComponent } from './workout-runner/workout-runner.component';
import { StartComponent } from './start/start.component';
import { FinishComponent } from './finish/finish.component';
const routes: Routes = [
{ path: 'start', component: StartComponent },
{ path: 'workout', component: WorkoutRunnerComponent },
{ path: 'finish', component: FinishComponent },
{ path: '**', redirectTo: '/start' }
];
@NgModule({
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }
An important thing to note, is the “why” of why we have the route configuration in its own module. Yes, it could be possible to have the thing in the AppModule itself, but as the number and complexity of routing grows, it’s a good practice to have routing in its own module and import that from AppModule.
Pushstate API and server-side url-rewrites
A request comes into https://something/, the index.html is served, and with that the application’s JS. If an user navigates to https://something/some/other/path, due to the pushstate API no browser refresh will be made.
If the user decides to reload the page while on something other than the root of the application, then the request will be for something that probably only exists client side, triggering a 404 error. To fix this, the server must support the “url-rewrite” feature, where anything that would otherwise return a 404 must return index.html instead.
IIS does support this, of course: https://www.iis.net/downloads/microsoft/url-rewrite
RouterOutlet
The router outlet is like the @RenderBody of Angular. It renders whatever component is active according to the current route.
<router-outlet></router-outlet>
Route navigation
It happens when:
- The user enters a URL in the browser address bar.
- After clicking a link on the anchor tags
- On using a script/code to navigate
localhost/#/start can also be used, but the useHash property must be set to true in the RouterModule.forRoot function.
<a routerLink="/workout">Start</a>
The route is fixed, and we’re doing one-time binding. The “/” denotes an absolute route path, not a relative path.
The link parameter array
Given:
@RouteConfig([
{ path: '/users/:id', component: UserDetail },
{ path: '/users', component: UserList},
])
Then:
<a [routerLink]="['/users', 2] // generates /users/2
In general:
['routePath', param1, param2, {prop1:val1, prop2:val2} ....]
And for optional parameters:
<a [routerLink]="['/users', {id:2}] // generates /users;id=2
Navigating with the router service
Getting the router instance:
import {Router} from '@angular/router';
...
constructor(private router: Router) {
The router gets injected due to the magic of the Angular’s DI framework.
Navigating:
this.router.navigate( ['/finish'] );
Dynamic route’s parameters
ActivatedRoute service
Given this path:
{ path: '/users/:id', component: UserDetailComponent },
The UserDetailComponent can get the id by requiring on ActivatedRoute:
export class UserDetailComponent {
constructor( private route: ActivatedRoute ...
Then anywhere else it can get the id:
ngOnInit() {
let id = +this.route.paramMap.get('id'); // (+) converts string 'id' to a number
var currentUser=this.getUser(id)
}
Angular Dependency Injection
There are two main ways of making something available for dependency injection. The first, is to decorate something with the @Injectable decorator as follows:
@Injectable({
providedIn: CoreModule
})
MySuperService ...
or to do so in the module itself:
@NgModule({ providers: [ MySuperService ] })
export class CoreModule {}
They’re equivalent for all intents and purposes (TODO: verify how true is this).
In the case of @Injectable we assign the value ‘root’ to providedIn, which would create a provider with the root injector. This is confusing, because even if we include the service as part of a module as we did with our two examples, the service is still registered via a provider with the root injector, meaning it will be available application wide regardless of whether the module is imported or not.
Services could be also be registered by a component, but this is a more advanced topic that’s not covered yet.
Providers create dependencies when the Angular injector requests them. These providers have the recipe to create these dependencies.
Classes are not the only things that can be provided by providers. An specific object or value can be provided, and a factory function could be provided too.
Angular providers
This:
providers: [ MySuperService ]
is equivalent to this:
providers: ({ provide: MySuperService, useClass: MySuperService })
The “provide” property is the “DI token”, which is what the DI framework uses to know when to inject a given dependency using the provider. The “useClass” property is the provider itself, more specifically in this case a Class Provider.
There are other providers too.
Value providers
{ provide: MySuperService, useValue: new MySuperService() }
Here we provide an instance manually, just remember that any transitive dependencies would require us to provide them manually.
!!! Whenever possible use a Class Provider instead of a Value Provider.
Factory providers
{provide: WorkoutHistoryTrackerService, useFactory: (environment:Environment) => {
if(Environment.isTest) {
return new MockWorkoutHistoryTracker();
}
else {
return new WorkoutHistoryTrackerService();
},
deps:[Environment]
}
A factory provider can also have their own dependencies, which are listed in “deps”.
Dependency tokens
As said, the injector knows when to inject a dependency or not based on DI tokens. Until now, we’ve been using Class Tokens:
{ provide: MySuperService, ... }
^
|----- class token
But there are cases when the dependency is not a class, or maybe we want more control over the injection. For those cases we have InjectionToken and string tokens.
InjectionToken
- Create a
InjectionToken
instance: export const APP_CONFIG = new InjectionToken(‘Application Configuration’); - Use the token to register the dependency: { provide: APP_CONFIG, useValue: {name:‘Test App’, gridSetting: {…} …});
- Inject the dependency somewhere using the @Inject decorator: constructor(@Inject(APP_CONFIG) config) { }
string tokens
They’re much simpler than injection tokens, just don’t msisspell them:
{ provide: 'appconfig', useValue: {name:'Test App', gridSetting: {...} ...});
...
constructor(@Inject('appconfig') config) { }
Angular Services
Services go into the CoreModule, as per the Angular style guide.
To generate the CoreModule, and import it from the AppModule:
ng generate module core --module app
To generate the service in question, from the CoreModule directory:
ng generate service workout-history-tracker
A service is just a class, that’s @Injectable. That decorator makes it available for injection by the DI framework. The providedIn: ‘root’ property tells Angular to create a provider with the root injector (??????).
Providers tell the injector how to create the service. Without a provider, the injector would not know that it is responsible for injecting the service nor be able to create the service.
A service in Angular is just a class that has been registered with Angular’s DI framework. Nothing special about them!
Sometimes it’s desirable to include the service as part of a module instead.
OPTION 1:
@Injectable({
providedIn: CoreModule
})
export class WorkoutHistoryTrackerService {
OPTION 2:
@NgModule({
providers: [WorkoutHistoryTrackerService],
})
export class CoreModule { }
Yep, that’s what the providers does.
Unless we need otherwise, it’s preferable to use the @Injectable because Angular CLI can perform tree shaking to leave out any service that’s declared but not used.
Registering services at the module level may be advantageous when lazy loading is involved.
The Router service uses the Location service internally to interact with the URL. We can use the location service too to trivially go back to the previous page.
contructor(location: Location) {}
...
this.location.back()
There’s a orderBy pipe and a search pipe whose code we can copy from the checkpoint3.4 from the example app.
Stateful pipes
The search pipe has an additional property in its @Pipe decorator. The pure
attribute,
set to false.
@Pipe({
name: 'search',
pure: false
})
The pure attribute determines whether a pipe is stateless (aka. pure) or not. A stateless pipe only is re-evaluated if its input changes. A strict (===) comparison is made to determine whether the old state is different from the current state. For reference types, this means a change in assignment/ref. change; there’s no deep comparison.
While a possible solution would be to make the array immutable, and replaced with every change, another option is to tell the pipe to re-evaluate regardless of whether the input “changed” or not. This is made by setting the @Pipe’s pure property to false, theremore turning the pipe into a “stateful pipe”.
Careful though, because it may be needed for us to return a new array from a pipe, for a change in size to be detected (actually what gets detected is the new ref.)
if (searchTerm == null) return [...value];
Change detection overview
As said, changes in property values are determined by strict comparison. No deep comparison is done.
When the change detection is triggered, well, change detection happens. Change detection happens in two passes: first the component tree is walked, and components that need to be refreshed due to model updates are marked. Then the view is synced with the underlying model.
What about trigger conditions?
- User input/browser events.
- Remote XHR
- setTimeout & setInterval.
Hierarchical injectors
Dependencies are singleton by nature, shared among components.
Every component has an injector, often shared with other components. There can be many injector instances operating at different levels of the component tree. It’s useful to pretend that every component has its own injector.
Angular DI dependency walk
Root Component (root injector) - Sub-component (other injector) - sub sub component –(depends on)–> some-dependency - other sub component
Components try to resolve dependencies with their own injector. If they fail, they try to do so with their parent’s. This process repeats until the resolution is tried with the root injector.
- If a dependency is registered with a module, then it becomes registered with the root injector.
- It’s possible to “override” dependency registration in “lower” components, overriding module registrations.
- If a dependency is registered with a component, then it’s bound to the
component’s lifecycle.
- i.e. created every time the component is loaded; destroyed when the component is destroyed.
@Injectable
The @Injectable decorator is the preferred way of registering services since Angular 6. It mostly has some advantages regarding tree-shaking, but I’m not taking notes about that for now.
It’s advisable to use the @Injectable decorator even if you register the dependency in a module (via providers), as doing so will force the TS compiler to compile metadata into the service, which Angular will use later to resolve the dependencies of the service itself, if it has any.
OF COURSE DO NOT ADD THE providedIn
PROPERTY IF YOU ALREADY REGISTERED THE SERVICE
SOMEWHERE ELSE.
Directives
ng generate directive my-audio
In Angular, the only place where directDOM manipulation is in directives.
@Directive({
selector: 'some-selector', // selector is attr based by default, but it can be
// element based as it's now
exportAs: 'MyDirective'
})
TODO: Take more notes about Angular directives
Cross-component communication
Template reference variables
A template reference variable is a way of capturing a reference to a specific element, component, or directive so that it can be used somewhere else in the same template.
Template reference variables are recognized by the use of the “#” (octothorpe) symbol.
<audio #ticks="MyAudio" ...></audio> <!--- Explicit directive type -->
<workout-runner #runner></workout-runner> <!--- component type --->
<input #emailId type="email"> <!--- DOM type --->
We can inject the template variables in our components by using the @ViewChild and @ViewChildren decorators.
@ViewChild('ticks') private ticks: MyAudioDirective;
@ViewChildren(MyAudioDirective) allAudios: QueryList<MyAudioDirective>;
In the case of ViewChildren, we would inject all the template variables of type MyAudioDirective.
@Output events & EventEmitter
@Output() exercisePaused: EventEmitter<number> = new EventEmitter<number>();
...
this.exercisePaused.emit(this.currentExerciseIndex);
Parent -> Child comms.
There are at least four ways of sending messages from a parent component to a children component.
- Property binding
<workout-audio [stopped]="workoutPaused"></workout-radio>
- Get a reference to the child component, then call it’s functions directly.
// Use template variables and @ViewChild to do this.
- Inject thfe parent component into the children component
// Not recommended. Not taking notes about this option.
- Sibling component interaction using events and template variables
<abe-workout-runner (exercisePaused)="wa.stop()"
(exerciseResumed)="wa.resume()"
(exerciseProgress)="wa.onExerciseProgress($event)"
...></abe-workout-runner>
<abe-workout-radio #wa></abe-workout-radio>
===== Notas miscelaneas de partes futuras siguen =====
Child routing
Registering ALL the components and their routes in the root router would certainly defeat the purpose of doing component based development. To prevent this, you can delegate the registration of their routes to some modules, instead. This is known as child routing, because the modules will define and export “child routers”, which will get registered with the “root router” when the AppModule imports those modules.
You can add the child router boilerplate when creating a new module by adding
the --routing
option to ng generate module
.
Regardless of whether you create a separate module for routing, or if you inline it in the module that requires routing itself, the route configuration looks as follows:
const routes: Routes = [
{
path: 'builder',
component: WorkoutBuilderComponent,
children: [
{path: '', pathMatch: 'full', redirectTo: 'workouts'}, // note pathMatch: 'full'
{path: 'workouts', component: WorkoutsComponent },
{path: 'workout/new', component: WorkoutComponent },
{path: 'workout/:id', component: WorkoutComponent },
{path: 'exercises', component: ExercisesComponent},
{path: 'exercise/new', component: ExerciseComponent },
{path: 'exercise/:id', component: ExerciseComponent }
]
},
];
The first “path” sets the base URL for the child routes, so every child route path
will be prepended by “builder” in this example. The first “component”, i.e. the
WorkoutBuilderComponent is where the child routes will be rendered, so it’s where
a
With the configuration in order, the module needs to export the router configuration as follows:
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class WorkoutBuilderRoutingModule { }
It’s VERY similar to the export in app.routing-module.ts, but the difference is that
instead of being forRoot
, now is forChild
. The forRoot
creates the Router service,
but forChild
does not. Remember that we cannot have more than one router service active
in our application.
Lazy loading of routes
From the module that loads the module that’s lazy, in the route configuration:
const routes: Routes = [
...
{ path: 'builder', loadChildren: './workout-builder/workout-builder.module#WorkoutBuilderModule' }
];
In the lazy module itself, then the base route should be changed to empty string then:
export const Routes = [
{
path: '',
...
}
]
Finally, the lazy module shouldn’t be imported directly from the app module.
Then the lazy module will only be loaded when the user visits the ‘builder’ path. This not only means that it’ll be instantiated lazily, but it’ll be also downloaded lazily.
HTTP service
export class HeroService {
constructor(private http: Http) { }
getHeroes() {
return this.http.get('api/heroes').pipe(
map((response: Response) => <Hero[]>response.json()));
}
}
Authorization example
User context could be shared using an Angular service, injected into components that require such context.
class SessionContext {
currentUser():User { ... };
isUserInRole(roles:Array<string>):boolean { ...};
isAuthenticated:boolean;
}
Routes could be restricted by means of implementing CanActivate
export class AuthGuard implements CanActivate {
constructor(private session:SessionContext) { }
canActivate() {
return this.session.isAuthenticated &&
session.isUserInRole(['Contributor', 'Admin']);
}
}
Conditional rendering could be handled by a structural directive like the following:
@Directive({ selector: '[a2beRolesAllowed]' })
export class RolesAllowedDirective {
private _prevCondition: boolean = null;
constructor(private _viewContainer: ViewContainerRef,
private _templateRef: TemplateRef, private SessionContext _session) { }
@Input() set a2beRolesAllowed(roles: Array<string>) {
if (this._session.isUserInRole(roles)) {
this._viewContainer
.createEmbeddedView(this._templateRef);
}
else {
this._viewContainer.clear();
}
}
}
Which is used as follows:
<div id='header'>
<div> Welcome, {{userName}}</div>
<div><a href='#/setting/my'>Settings</a></div>
<div *a2beRolesAllowed='["admin"])'>
<a href='#/setting/site'>Site Settings</a>
</div>
</div>
Authentication in Material Dashboard PRO Angular
Routes can be protected by adding our AuthGuard to the canActivate array of the route we wish to protect.
export const AppRoutes: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
},
{
path: '',
component: AdminLayoutComponent,
canActivate: [AuthGuard], //added canActivate and AuthGuard service
children: [
{
path: '',
loadChildren: './dashboard/dashboard.module#DashboardModule'
}
]
},
{
path: '',
component: AuthLayoutComponent,
children: [{
path: 'pages', //login page still under "pages" folder as the original code
loadChildren: './pages/pages.module#PagesModule'
}]
}
];
The AuthGuard itself does the validations we require, and in this case we add a returnUrl too:
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService: AuthService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable | Promise | boolean {
if (!this.authService.isLoggedIn()) {
this.router.navigate(['pages/login'], { queryParams: { returnUrl: state.url }});
}
return true;
}
}
Of course, you could optionally redirect logged-in users that visit the login page to some other page instead, like the dashboard for example:
// some component
constructor(private element: ElementRef, public authService: AuthService,
private router: Router, private route: ActivatedRoute) {
if (this.authService.isLoggedIn()) {
this.router.navigate(['dashboard']);
} else {
this.nativeElement = element.nativeElement;
this.sidebarVisible = false;
}
}
Resolve route guard
The guard itself:
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Router, Resolve, RouterStateSnapshot,
ActivatedRouteSnapshot } from '@angular/router';
import { WorkoutPlan } from '../../core/model';
import { WorkoutBuilderService } from '../builder-services/workout-builder.service';
@Injectable()
export class WorkoutResolver implements Resolve<WorkoutPlan> {
public workout: WorkoutPlan;
constructor(
public workoutBuilderService: WorkoutBuilderService,
public router: Router) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): WorkoutPlan {
let workoutName = route.paramMap.get('id');
if (!workoutName) {
workoutName = '';
}
this.workout = this.workoutBuilderService.startBuilding(workoutName);
if (this.workout) {
return this.workout;
} else { // workoutName not found
this.router.navigate(['/builder/workouts']);
return null;
}
}
}
Provide the guard at the module level, so isn’t loaded for unrelated modules.
....
import { WorkoutResolver } from './workout/workout.resolver';
@NgModule({
....
providers: [WorkoutBuilderService, WorkoutResolver]
})
....
Usage in route registration:
...
{path: 'workout/new', component: WorkoutComponent, resolve: { workout: WorkoutResolver} },
{path: 'workout/:id', component: WorkoutComponent, resolve: { workout: WorkoutResolver} },
...
Extracting the prefetched data from the guarded component:
constructor(
public route: ActivatedRoute, // <--- added the route to the ctor
public workoutBuilderService:WorkoutBuilderService){ }
ngOnInit() {
this.sub = this.route.data
.subscribe(
(data: { workout: WorkoutPlan }) => {
this.workout = data.workout;
}
);
}
Reactive Forms
FormGroup, FormControl, FormArray
We don’t need to use ngModel, but it could be used, at least in Angular 6.
ReactiveFormsModule contains what we need to use reactive forms.
Import to component:
import { Validators, FormArray, FormGroup, FormControl, FormBuilder } from '@angular/forms';
We use the FormBuilder class to define how our forms work. In the component’s constructor, as follows:
constructor(
public route: ActivatedRoute,
public router: Router,
public exerciseBuilderService: ExerciseBuilderService,
public formBuilder: FormBuilder
) {}
FormBuilder API
In our component:
public videoArray: FormArray = new FormArray([]);
ngOnInit() {
...
this.buildExerciseForm();
}
buildExerciseForm(){
this.exerciseForm = this.formBuilder.group({
'name': [this.exercise.name, [Validators.required, AlphaNumericValidator.invalidAlphaNumeric]],
'title': [this.exercise.title, Validators.required],
'description': [this.exercise.description, Validators.required],
'image': [this.exercise.image, Validators.required],
'nameSound': [this.exercise.nameSound],
'procedure': [this.exercise.procedure],
'videos': this.addVideoArray()
})
}
addVideoArray(){
if(this.exercise.videos){
this.exercise.videos.forEach((video : any) => {
this.videoArray.push(new FormControl(video, Validators.required));
});
}
return this.videoArray;
}
In our HTML view:
<!-- This connects the form element with our form code in our component --->
<form class="row" [formGroup]="exerciseForm" (ngSubmit)="onSubmit(exerciseForm)">
<!-- formControlName matches what we have in the exerciseForm --->
<input name="name" formControlName="name" class="form-control" id="name" placeholder="Enter exercise name. Must be unique.">
Showing validation errors:
<label *ngIf="exerciseForm.controls['name'].hasError('required') && (exerciseForm.controls['name'].touched || submitted)" class="alert alert-danger validation-message">Name is required</label>
FormArrays in Templates
The formControlName is “i”, as per the index of the current iteration. How does it know however that it refers to the index of the iterated videoArray.controls?
<label>Videos:</label>
<div *ngFor="let video of videoArray.controls; let i=index" class="form-row align-items-center">
<div class="col-sm-10">
<input type="text" class="form-control" [formControlName]="i" placeholder="Add a related youtube video identified."/>
</div>
<div class="col-auto my-1">
<span class="btn alert-danger" title="Delete this video." (click)="deleteVideo(i)">
<span class="ion-ios-trash-outline"></span>
</span>
</div>
<label *ngIf="exerciseForm.controls['videos'].controls[i].hasError('required') && (exerciseForm.controls['videos'].controls[i].touched || submitted)" class="alert alert-danger validation-message">Video identifier is required</label>
</div>
Saving the form example
onSubmit(formExercise: FormGroup) {
this.submitted = true;
if (!formExercise.valid) { return; }
this.mapFormValues(formExercise);
this.exerciseBuilderService.save();
this.router.navigate(['/builder/exercises']);
}
mapFormValues(form: FormGroup) {
this.exercise.name = form.controls['name'].value;
this.exercise.title = form.controls['title'].value;
this.exercise.description = form.controls['description'].value;
this.exercise.image = form.controls['image'].value;
this.exercise.nameSound = form.controls['nameSound'].value;
this.exercise.procedure = form.controls['procedure'].value;
this.exercise.videos = form.controls['videos'].value;
}
Custom Validators
In its simplest form, an Angular custom validator is a function that takes a control as an input parameter, runs the validation check and returns true or false.
// src/app/workout-builder/alphanumeric-validator.ts
export class AlphaNumericValidator {
static invalidAlphaNumeric(control: FormControl): { [key: string]: boolean } {
if ( control.value.length && !control.value.match(/^[a-z0-9]+$/i) ) {
return {invalidAlphaNumeric: true };
}
return null;
}
}
Integrating the validator is simple:
buildExerciseForm(){
this.exerciseForm = this._formBuilder.group({
'name': [this.exercise.name, [Validators.required, AlphaNumericValidator.invalidAlphaNumeric]],
. . . [other form controls] . . .
});
}
Showing the error message if needed is similar to what it was required for the included “required” validator.
<label *ngIf="exerciseForm.controls['name'].hasError('invalidAlphaNumeric') && (exerciseForm.controls['name'].touched || submitted)"
class="alert alert-danger validation-message">
Name must be alphanumeric
</label>
Server-side data persistence
import { HttpClientModule } from ‘@angular/common/http’; // app.module.ts
The HttpClient
service is the class that you need to inject in
components that do HTTP operations.
Any of the methods of the HTTP client return Observables (RxJS), which are implementations of the “async observable pattern”.
While it’s possible to work with promises, by calling toPromise
in
an observable, the default way of working is by using observables.
this.workoutService.getExercises()
.subscribe(
exercises => this.exerciseList = exercises,
(err: any) => console.error
)
About subscribing
No data flows through the observable unless there’s something subscribed to it. Remember this when doing adds and updates.
catchError
This determines what to do in case of an error.
getWorkouts() {
return this.http.get<WorkoutPlan[]>(this.collectionsUrl + '/workouts' + this.params)
.pipe(catchError(WorkoutService.handleError));
}
WorkoutService.handleError is defined as follows:
static handleError (error: Response) {
console.error(error);
return Observable.throw(error || 'Server error');
}
forkJoin
import { forkJoin } from 'rxjs/observable/forkJoin';
forkJoin
allows us to return multiple observable streams once
they finished retrieving their data.
getWorkout(workoutName: string): Observable<WorkoutPlan> {
return forkJoin (
this.http.get(this.collectionsUrl + '/exercises' + this.params),
this.http.get(this.collectionsUrl + '/workouts/' + workoutName + this.params))
.pipe(
map(
(data: any) => {
const allExercises = data[0];
const workout = new WorkoutPlan(
data[1].name,
data[1].title,
data[1].restBetweenExercise,
data[1].exercises,
data[1].description
);
workout.exercises.forEach(
(exercisePlan: any) => exercisePlan.exercise = allExercises.find(
(x: any) => x.name === exercisePlan.name
)
);
return workout;
}
),
catchError(this.handleError<WorkoutPlan>(`getWorkout id=${workoutName}`))
);
}
Mapping
getWorkouts(): Observable<WorkoutPlan[]> {
return this.http.get<WorkoutPlan[]>(this.collectionsUrl + '/workouts' + this.params)
.pipe(
map((workouts: Array<any>) => {
const result: Array<WorkoutPlan> = [];
if (workouts) {
workouts.forEach((workout) => {
result.push(
new WorkoutPlan(
workout.name,
workout.title,
workout.restBetweenExercise,
workout.exercises,
workout.description
));
});
}
return result;
}),
catchError(this.handleError<WorkoutPlan[]>('getWorkouts', []))
);
}
async pipe
Instead of subscribing to every observable and manually assigning
the results to a non-observable variable, we can use the async
instead, as follows:
<div *ngFor="let exercise of exerciseList|async|orderBy:'title'">
Route Guards
Route guards can prevent the activation of a component in a given route for whatever reason we want. There are 4 interfaces that we can implement to create route guards:
- CanActivate: mediates whether navigation to a route (and component activation) is possible.
- CanActivateChild: mediates whether navigation to ANY children route of the current route is possible.
- CanDeactivate: mediates whether navigating AWAY from the current route is possible. Often used for “are you sure you wanna discard your changes?” types of messages.
- Resolve: is about retrieving data for a route before the route loads. Often used to move the checks of whether a element exists or not outside from the components themselves.
- CanLoad: similar to CanActivate, but this determines whether an async feature module can be loaded or not.
CanActivate and CanActivateChild
canActivate[Child](next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean)
In Angular 7 it can also return UrlTree to navigate somewhere else. It’s still possible
to manually navigate using the router, but you must return false
after navigating
away.
You can inject required services in the constructor like always, nothing special there. What’s more important to understand though, is how and when the guards are called.
Suppose we have the following route & component structure:
'' -> AdminLayoutComponent
'dashboard' -> DashboardComponent
'user'
'' -> UserListComponent
'create' -> UserCreateComponent
'role'
'' -> RoleListComponent
'create' -> RoleCreateComponent
'edit/:id' -> RoleEditComponent
'/login' -> LoginComponent
If we were to add a CanActivate guard in ’’ (AdminLayoutComponent), then it would get called the first time we try to access any of its child routes. But after AdminLayoutComponent has been activated, the guard will not get called while we navigate in any of the children routes. If we navigate to ‘/login’ and then come back to ’’ (or if we reload the page) then the guard will be checked again.
If we added a CanActivateChild guard in ’’ (AdminLayoutComponent), it will get called every time we navigate to children routes of ‘’, recursively.
Visiting ‘/role/edit/5’, would check the CanActivateChild guard twice: once for ‘role’ (child route of ‘’), and once for ’edit/:id’ (child route of ‘role’).