Post

Angular

Angular

Table of Contents

Angular

Installation, initialization

  1. 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
  2. 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"]
  3. 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
  4. 🛠️ npm install problems
    • When getting errors with npm install, a solution may be to run npm install --legacy-peer-deps instead of npm install.
  5. 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
  6. To run app use
    • ng serve

Selectors

  1. Selector
    • It’s the way Angular selects an element
  2. Available Selectors
    1. Element Selector
      • Used for Components
      • my-component -> <my-component></my-component>
        • e.g. @Component({ selector: 'app-recipes', (...) }}
    2. Attribute Selector
      • [my-component] -> e.g. <div my-component></div>
        • e.g. @Directive({ selector: '[my-component]', (...) })
    3. Class Selector
      • .my-component -> e.g. <div class="my-component"></div>
    4. ID Selectors, PseudoSelectors (like hover)
      • Not supported

Data Binding

  1. Data Binding
    • It can be described as communication between:
      • Business Logic Layer (TypeScript)
      • Presentation Layer (HTML)
  2. Data Binding possibilities
    1. Output Data (from TS to HTML)
      1. String Interpolation ``
      2. Property Binding - input: [disabled]="userIsGuest", a: [url]="recipe.url"
    2. Input (React to Events)
      • Event Binding (event)="someExpression"
    3. 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
    • @ViewChild(MyComponent)
  • 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 (@Inputs)
    • It receives a SimpleChanges object
  • ngOnInit() - Component is initialized (it is not not yet added to DOM)
    • Runs after constructor
  • ngDoCheck() - runs whenever Angular change detection runs, e.g. button clicked
  • ngAfterContentInit() - whenever <ng-content> was projected into the View
  • ngAfterContentChecked() - every time projected content was checked
  • ngAfterViewInit() - after Component’s View and child Views was initialized (rendered)
  • ngAfterViewCheck() - every time Component’s View and child Views was checked
  • ngOnDestroy() - 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
  1. Add @Injectable({providedIn: 'root'}) Decorator
  2. Or before Angular 6:
    • Add in AppModule
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

  1. Angular actually replaces Component tags like <app-root></app-root> with HTML defined in corresponding .html files
  2. 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
  3. Angular CLI injects into <script> tag code that starts Angular into index.html
  4. The first code that gets executed is main.ts file
  5. By default, directories aren’t scanned, so you have to specify Components in app.module.ts in @NgModule in declarations property
  6. 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 it
    • Emulated - like real ShadowDOM, but it’s emulated, so all browsers will handle it properly
  • You can set it in:
    • @Component({ encapsulation: ViewEncapsulation.None })

Definitions

  1. Decorator
    • @Something <- this is Decorator
    • Java Annotations are TypeScript Decorators

Various

Various

  1. 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
  2. innerText ~ ``
    • Both these expressions are equivalent:
      • <p [innerText]="myValue"></p>
      • <p></p>
  3. Shortcut for defining class fields
    • Put them into constructor like
      • constructor(public field: string, public anotherField: int)
  4. When setting name for Component selector, we prefix it with app- to avoid conflicts with “real” HTML selectors.

Emmet

  1. 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>
  2. Create div with given class
    • creates div with given class
    • .container -> <div class="container"></div>
  3. Create nested HTML
    • .row>.col-xs-12 -> <div class="row"><div class="col-xs-12"></div></div>

Bootstrap

  1. 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
1
ng test
  • ng test
    1. Finds all spec.ts files
    2. Compiles into JavaScript Bundle using Webpack
    3. Initializes TestBed Angular Testing Environment
    4. Launches Karma Test Runner
    5. Karma launches Browser(s)
    6. 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

  1. TestBed creates a dynamically-constructed Angular test module that emulates an Angular @NgModule.
  2. TestBed.configureTestingModule() method takes a metadata object that can have most of the properties of an @NgModule.
  3. 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
  4. Inject it then using const service = TestBed.inject(MyService)
  5. 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

  1. ❗ Always provide both next and error callbacks for Observables.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.
  2. 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

  1. Remember to manually call lifecycle hook methods like ngOnInit (just as Angular would do)

DOM Testing

  1. 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.
  2. Basic Component test assertion
    • Component can be created
    • expect(component).toBeDefined();
  3. ❗ 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 NativeElements 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


💠✔️💭💬🗨️