Table of Contents
Angular
Installation, initialization
- To create Angular app, install Angular CLI and execute
ng new my-app --no-strict --standalone false --routing false
--no-strict
at a beginning will help learning by not using strict mode--standalone false
as standalone
is some other way of working with Angular--routing false
as for the beginning routing is not needed
- In
angular.json
you can specify style sources- Ordering matters, so latter ones will override former ones
- Set it in
projects.first-app.architect.build.options.styles
property- e.g.
"styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.css"]
- You can use Bootstrap for out-of-the-box useful styles
npm install --save bootstrap@3
- Then reach your CSS located in
node_modules/bootstrap/dist/css/bootstrap.css
- You can use minified file, which is smaller:
bootstrap.min.css
- You add the style to
angular.json
in a way mentioned above
- 🛠️
npm install
problems- When getting errors with
npm install
, a solution may be to run npm install --legacy-peer-deps
instead of npm install
.
- Create Component from CLI
ng generate component my-component
- or in short:
ng g c my-component
- You can exclude creating tests with
--skip-tests
- To run app use
Selectors
- Selector
- It’s the way Angular selects an element
- Available Selectors
- Element Selector
- Used for Components
my-component
-> <my-component></my-component>
- e.g.
@Component({ selector: 'app-recipes', (...) }}
- Attribute Selector
[my-component]
-> e.g. <div my-component></div>
- e.g.
@Directive({ selector: '[my-component]', (...) })
- Class Selector
.my-component
-> e.g. <div class="my-component"></div>
- ID Selectors, PseudoSelectors (like hover)
Data Binding
- Data Binding
- It can be described as communication between:
- Business Logic Layer (TypeScript)
- Presentation Layer (HTML)
- Data Binding possibilities
- Output Data (from TS to HTML)
- String Interpolation ``
- Property Binding - input:
[disabled]="userIsGuest"
, a: [url]="recipe.url"
- Input (React to Events)
- Event Binding
(event)="someExpression"
- Output & Input (Two-Way) Data Binding
[(ngModel)]="data"
- Example:
<input type="text" [(ngModel)]="myVariable">
- Listen to everything that is inputted and save it in
myVariable
- At the same time listen to this variable and update
<input>
text accordingly
$event
- Syntax for data emitted by a given event
- Like keystroke in input
<input type="text" (input)="onKeystroke($event)">
Directives
Directives
- Instructions in the DOM
- Structural Directives
- It means it changes the structure of the DOM
- It doesn’t hide/unhide, it really adds/removes elements
- It can be only one Structural Directive on an element
- Structural Directives start with a star
*
- Example:
*ngIf
, *ngFor
- Attribute Directives
- They allow to manipulate attributes
- They change only the element they sit on
- Example:
ngStyle
Create *ngIf
with else
- Create
*ngIf
element<p *ngIf="myPredicate"; else myReference>Predicate is satisfied</p>
- Create a local reference in an “else” element
<ng-template #myReference><p>Predicate not satisfied</p></ng-template>
*ngFor
- Get index of current iteration
*ngFor="let item of items; let i = index"
Custom Directives
Example
Write directive.ts code
1
2
3
4
5
6
7
8
9
| @Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
constructor(private elementRef: ElementRef, private renderer: Renderer2) { }
ngOnInit() {
this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'yellow');
}
}
|
Add Directive to @NgModule
declarations
:
1
2
3
4
5
6
| @NgModule({
declarations: [
AppComponent,
HighlightDirective
]
})
|
Generating Directives from CLI
ng generate directive improved-highlight
ng g d improved-highlight
Directives allow to listen to events
1
2
3
| @HostListener('mouseenter') mouseenter(event: Event) {
this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'blue')
}
|
Structural Directives behind the scenes
Something like this
1
2
3
| <div *ngIf="x > 0">ł
<p>x is greater than 0</p>
</div>
|
Gets translated into:
1
2
3
4
5
| <ng-template [ngIf]="x > 0">
<div>
<p>x is greater than 0</p>
</div>
</ng-template>
|
@HostBinding
- Use it to bind with a certain property, e.g.
@HostBinding('style.backgroundColor') backgroundColor = 'green'
@HostBinding('class.open') isOpen = true
[ngSwitch]
example
1
2
3
4
5
| <div [ngSwitch]="myValue">
<p *ngSwitchCase="Yes">Yes, agreed.</p>
<p *ngSwitchCase="No">No, disagreed.</p>
<p *ngSwitchDefault>Can't say.</p>
</div>
|
Binding
Allow external components to “send” data
- HTML -> TS
@Input()
- Or binding through alias -
@Input('nameToOutsideComponents') innerValue
- Then in Parent you write
<my-component [nameToOutsideComponents]="parentValue">
- instead of
<my-component [innerValue]="parentValue">
Local reference
<input type="text" #myHTMLReference>
- Used to pass data HTML -> TS
- Used for passing data locally
- It passes the HTML Element itself (real reference)
- Can be placed on any HTML element
- Visible within the whole HTML Template
@ViewChild()
- Used to pass data HTML -> TS
- Used for local binding
- Access if after the
afterViewInit()
/afterViewCheck()
- ❗ You shouldn’t change element using this way, only get values
- This is discouraged way of modifying DOM
- If you want to use it in
ngOnInit()
add { static: true }
@ViewChild('localReference', { static: true })
- You can reference the first occurrence of a given type
- If you use it with local reference, it’s
LocalRef
type
<ng-content></ng-content>
- Directive used to include all HTML found within this tag
- By default all HTML within Component HTML tags is lost
@ContentChild()
- Used to pass data HTML -> child Component’s TS
- It passes local reference from content into the child component
@ContentChild('contentReference')
- If you want to use it in
ngAfterContentInit()
add { static: true }
@ContentChild('contentReference', { static: true })
Example
main.component.html
1
2
3
| <app-widget-component>
<p #contentParagraph>Some Content to be passed to WidgetComponent</p>
</app-widget-component>
|
widget.component.ts
1
| @ContentChild('contentParagraph', { static: true }) paragraph: ElementRef
|
Lifecycle Hooks
ngOnChanges()
- after on bound input property change (@Input
s)- It receives a
SimpleChanges
object
ngOnInit()
- Component is initialized (it is not not yet added to DOM)ngDoCheck()
- runs whenever Angular change detection runs, e.g. button clickedngAfterContentInit()
- whenever <ng-content>
was projected into the ViewngAfterContentChecked()
- every time projected content was checkedngAfterViewInit()
- after Component’s View and child Views was initialized (rendered)ngAfterViewCheck()
- every time Component’s View and child Views was checkedngOnDestroy()
- right before Component will be destroyed
Services
Inject Service
To make Service injected, add it into providers
property of @Component
directive (to tell Angular how to handle this service), and add it into constructor
:
1
2
3
4
5
6
7
| @Component({
// ...
providers: [MyService]
})
export class MyComponent {
constructor(private myService: MyService) {}
}
|
Hierarchical Injector
- It provides the same instance of a Service for a given Component and its all children.
- It’s important to note that therefore it’s not a pure
singleton
from Spring, but something like hierarchy singleton, therefore multiple instances of a given service/dependency may exist. - In short, it goes down, not up
Scopes
The whole Application
- Add
@Injectable({providedIn: 'root'})
Decorator - Or before Angular 6:
All Components
Placing instance in AppComponent
- makes it available in all Components
- (but not for e.g. other Services)
Component (and its children)
- Place it on Component
- ⚠️ this way it can override dependency provided on a higher level (like
AppModule
or AppComponent
)- 💡 So if you don’t want to duplicate dependency, don’t add it into
providers
array - place it only on constructor
Inject Service
To make Service injected into another Service:
- it must be in the Application-wide scope
- you have to add
@Injectable()
decorator for the class- (
@Injectable()
marks class as one into which we want to inject something) - However (since some version) it’s recommended to add this annotation also on classes we want to be injected
How it works
- Angular actually replaces Component tags like
<app-root></app-root>
with HTML defined in corresponding .html
files - Angular is for creating SPAs - Single Page Applications
- Even if we have Routing, etc. it’s still an SPA
- It is because there’s only one
index.html
- Angular CLI injects into
<script>
tag code that starts Angular into index.html
- The first code that gets executed is
main.ts
file - By default, directories aren’t scanned, so you have to specify Components in
app.module.ts
in @NgModule
in declarations
property - TL;DR is that Angular changes DOM (HTML) at runtime
How Angular handles CSSes
- It adds a certain unique property to all HTML elements in the given component and applies styles only on elements with matching property, e.g.
<p _ngcontent-ejo-1>This text should be red</p>
p[_ngcontent-ejo-1] { font-color: red; }
View Encapsulation
- There are 3 types:
None
- don’t encapsulate CSS, so it will be applies globally (se even in parent Components)Native
(ShadowDOM) - encapsulates CSS natively, but only in browsers which support itEmulated
- like real ShadowDOM, but it’s emulated, so all browsers will handle it properly
- You can set it in:
@Component({ encapsulation: ViewEncapsulation.None })
Definitions
- Decorator
@Something
<- this is Decorator- Java Annotations are TypeScript Decorators
Various
Various
- Inline HTML/CSS for Component
- You can define HTML template in
@Component
Decorator inline- instead of
templateUrl
property, use just template
- instead of
stylesUrl
, use styles
innerText
~ ``- Both these expressions are equivalent:
<p [innerText]="myValue"></p>
<p></p>
- Shortcut for defining class fields
- Put them into constructor like
constructor(public field: string, public anotherField: int)
- When setting name for Component selector, we prefix it with
app-
to avoid conflicts with “real” HTML selectors.
Emmet
- Emmet
- It’s IDE plugin, which helps to work with HTML, CSS, JSX by expanding abbreviations
- e.g.
app-component
, Tab
becomes <app-component></app-component>
- Create
div
with given class
- creates
div
with given class
.container
-> <div class="container"></div>
- Create nested HTML
.row>.col-xs-12
-> <div class="row"><div class="col-xs-12"></div></div>
Bootstrap
- Create button group
<div class="btn-group"></div>
Testing
WSL2 🔨
Running tests on WSL2 requires to handle Chrome executable. Therefore working with them is more straightforward on host system.
Testing
Run tests
- Build app in watch mode, launch Karma test runner
ng test
- Finds all
spec.ts
files - Compiles into JavaScript Bundle using Webpack
- Initializes TestBed Angular Testing Environment
- Launches Karma Test Runner
- Karma launches Browser(s)
- Tests are started
Customize Karma Test Runner and/or Jasmine
- Open
karma.conf.js
and update angular.json
- Or generate file if not exists
1
| ng generate config karma
|
Run tests in CI/CD
1
| ng test --no-watch --no-progress --browsers=ChromeHeadless
|
Generate Coverage Report
1
| ng test --no-watch --code-coverage
|
Some ways of injecting dependencies
Just use real production service
1
| new SubjectUnderTest(new ProdService())
|
Use fake service
1
| new SubjectUnderTest(new FakeService())
|
Use fake object
1
| new SubjectUnderTest({ someFunction: () => 'fake value' } as ProdService)
|
Use Spy
1
2
3
4
| const serviceSpy = jasmine.createSpyObj("ProdService", ['someFunction']);
const stubValue = "stub value";
serviceSpy.someFunction.and.returnValue(stubValue);
new SubjectUnderTest(serviceSpy);
|
TestBed
TestBed
creates a dynamically-constructed Angular test module that emulates an Angular @NgModule
.TestBed.configureTestingModule()
method takes a metadata object that can have most of the properties of an @NgModule
.- When you want to test a Service, use
providers
metadata property and specify there other Services you want to mock.TestBed.configureTestingModule({ providers: [ MyService, { provide: OtherService, useValue: spy } ] });
- Or if using real class, instead of
useValue
, use: useClass: TestOtherService
- Inject it then using
const service = TestBed.inject(MyService)
- You have to add
testBedComponent.detectChanges()
when you want to refresh state (e.g. MyComponent retrieves some data from MyService - it can happen even synchronously)
TestBed Example
1
2
3
4
5
6
7
8
9
10
| let myService: MyService;
let otherServiceSpy: jasmine.SpyObj<OtherService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('OtherService', ['someFunction']);
TestBed.configureTestingModule({providers: [MyService, {provide: OtherService, useValue: spy}]});
});
myService = TestBed.inject(MyService);
otherServiceSpy = TestBed.inject(OtherService) as jasmineSpyObj<OtherService>;
|
Testing HTTP Services
- ❗ Always provide both
next
and error
callbacks for Observable
s.Otherwise you may encounter other tests failing in random places!- It is because without specifying
error
, you end up with an asynchronous uncaught observable error.
HttpClientTestingModule
- Used for complex interactions
- It’s covered in HttpGuide
Example of HTTP test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| let httpClientSpy: jasmine.SpyObj<HttpClient>;
let myService: MyService;
beforeEach(() => {
httpClientSpy = jasmine.createObjSpy('HttpClient', ['get']);
myService = new MyService(httpClientSpy);
});
it('should return expected response', (done: DoneFn) => {
const expectedValue = 'correct value';
httpClientSpy.get.and.returnValue(asyncData(expectedValue));
myService.someFunction().subscribe({
next: (returnedValue) => {
expect(returnedValue).toEqual(expectedValue);
done();
},
error: done.fail,
});
expect(httpClientSpy.get.calls.count()).toBe(1);
});
|
Asynchronous testing
Example:
1
2
3
4
5
6
7
8
9
10
11
| it('should fetch data successfully if called asynchronously', async(() => {
const fixture = TestBed.createComponent(UserComponent);
const app = fixture.debugElement.componentInstance;
const dataService = fixture.debugElement.injector.get(DataService); // Old way
const spy = spyOn(dataService, 'getDetails')
.and.returnValue(Promise.resolve('Data')); // Change behavior of getDetails() method
fixture.detectChanges();
fixture.whenStable().then(() => { // Wait for async to finih
expect(app.data).toBe('Data');
});
}));
|
Testing Components
- Remember to manually call lifecycle hook methods like
ngOnInit
(just as Angular would do)
DOM Testing
- Component is more than just a class. It interacts with DOM and other Components.
- Therefore tests covering only a class won’t tell if Component renders, interacts with user or interact with other (e.g. parent or child) Components properly.
- You need to examine DOM and simulate user interactions to be able to confirm Component works as intended.
- Basic Component test assertion
- Component can be created
expect(component).toBeDefined();
- ❗ Don’t reconfigure
TestBed
after calling createComponent()
createComponent()
freezes the current TestBed
definition, which makes it closed to further modifications
async functions
When function call is asynchronous, you can use waitForAsync()
1
2
3
| beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ imports: [ BannerComponent ]}).compileComponents();
}));
|
createComponent()
TestBed.createComponent(MyComponent)
creates an instance of MyComponent
, adds it to the DOM and returns a ComponentFixture
ComponentFixture
It’s a harness for interacting with the created Component and its corresponding element
1
2
3
| const componentFixture = TestBed.createComponent(MyComponent);
const component = componentFixture.componentInstance;
expect(component).toBeDefined();
|
NativeElement
- It’s
any
type, as at a compile time it’s unknown what type it is or even if it’s an HTMLElement
- Tests may be run on a non-browser platform such as WebWorker or any other that doesn’t have DOM or where DOM-emulation doesn’t support full
HTMLElement
API
- In standard browser environment
nativeElement
will always be an HTMLElement
(or one of it’s derived classes) - You can use standard HTML API then, e.g.
querySelector()
1
2
3
| const bannerElement: HTMLElement = componentFixture.nativeElement;
const paragraph = bannerElement.querySelector('p')!;
expect(p.textContext).toEqual('Hello World');
|
- Under the hood it’s actually
fixture.debugElement.nativeElement
DebugElement
- An abstraction to work safely across all platforms
- Angular creates a
DebugElement
tree which wraps NativeElement
s for the running platform DebugElement
contains methods and properties useful in testing
Example of testing
1
2
3
| const bannerElement: HTMLElement = componentFixture.nativeElement;
const paragraph = bannerElement.querySelector('p')!;
expect(p.textContext).toEqual('Hello World');
|
Legend
❗ - highly important information
⚠️ - important information
💡 - tip, good practice
🛠️ - troubleshooting
💠✔️💭💬🗨️