Angular

How to fix Angular ROR Error: ngIfElse must be a TemplateRef, but received ‘[object HTMLDivElement]’.

Problem:

Your Angular template looks like this:

<div *ngIf="files.length > 0 ; else noFiles">
    <!-- ... -->
</div>
<div #noFiles>
    <!-- ... -->
</div>

but when you open it in the browser, you see the following stacktrace:

c_my_module.js:2 ERROR Error: ngIfElse must be a TemplateRef, but received '[object HTMLDivElement]'.
    at assertTemplate (common.mjs:3392:15)
    at set ngIfElse [as ngIfElse] (common.mjs:3328:9)
    at setInputsForProperty (core.mjs:11741:34)
    at elementPropertyInternal (core.mjs:10874:9)
    at ɵɵproperty (core.mjs:13637:9)
    at PlanungsunterlagenDashboardComponent_Template (planungsunterlagen-dashboard.component.html:8:55)
    at executeTemplate (core.mjs:10534:9)
    at refreshView (core.mjs:10419:13)
    at refreshComponent (core.mjs:11480:13)
    at refreshChildComponents (core.mjs:10210:9)

Solution:

The error message is telling you that #noFiles is a <div> but it must be a <ng-template>:

In order to fix it, change it to a ng-template:

<ng-template #noFiles>
    <!-- ... -->
</ng-template>

If you need a <div> specifically, you need to place it inside the ng-template:

<ng-template #noFiles>
    <div>
        <!-- ... -->
    </div>
</ng-template>

 

Posted by Uli Köhler in Angular, Javascript

How to fix Angular “ng build” not producing index.html

Unless you have errors in your build (which is clearly visible from looking at the ng build output), the reason why ng build doesn’t produce an index.html is that the resulting bundle exceeds the maximum allowed size.

This is evident from the output such as

Error: bundle initial exceeded maximum budget. Budget 1.00 MB was not met by 3.73 kB with a total of 1.00 MB.

In order to fix it, either shrink your application by removing unneeded features or libraries or splitting off certain features into extra modules or, at least temporarily, increase the allowable budget.

In order to do that, open angular.json and look for the "budgets": {... section:

"budgets": [
  {
    "type": "initial",
    "maximumWarning": "500kb",
    "maximumError": "1mb"
  },
  {
    "type": "anyComponentStyle",
    "maximumWarning": "2kb",
    "maximumError": "4kb"
  }
],

Increase the maximumError value for "type": "initial" (the first entry in the list shown above).

For example, you could increase it from 1mb to 4mb to fix your build.

After that, run your ng build command again and index.html should be generated.

Be aware, however, that having huge JS bundles can really slow down your web application especially for mobile users.

Posted by Uli Köhler in Angular

Angular HttpClient: Observable with data continous data stream using interval requests to API endpoint

The following example requests /api/data every second, creating an Observable where you can subscribe to the continous value stream.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { interval, mergeMap, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  constructor(private http: HttpClient) { }

  continousDataStream(_interval=1000): Observable<any> {
    return interval(_interval).pipe(mergeMap(_ => {
        return this.http.get<any>(`/api/data`);
    }));
  }
}

 

Posted by Uli Köhler in Angular

How to fix Angular NullInjectorError: No provider for HttpClient!

In case you see the following error for your Angular application in your JS console:

main.ts:6 ERROR NullInjectorError: R3InjectorError(AppModule)[MyService -> HttpClient -> HttpClient -> HttpClient]: 
  NullInjectorError: No provider for HttpClient!
    at NullInjector.get (core.mjs:7599:27)
    at R3Injector.get (core.mjs:8020:33)
    at R3Injector.get (core.mjs:8020:33)
    at R3Injector.get (core.mjs:8020:33)
    at injectInjectorOnly (core.mjs:634:33)
    at Module.ɵɵinject (core.mjs:638:60)
    at Object.MyService_Factory [as factory] (my.service.ts:8:38)
    at R3Injector.hydrate (core.mjs:8121:35)
    at R3Injector.get (core.mjs:8009:33)
    at ChainedInjector.get (core.mjs:12179:36)

you need to add HttpClientModule to your app.module.ts.

First, import HttpClientModule at the top of app.module.ts:

import { HttpClientModule } from '@angular/common/http';

After that, add

HttpClientModule,

to the imports: [...] section of your @NgModule, for example:

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    HttpClientModule,
    BrowserModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

After that, your error should have disappeared

Posted by Uli Köhler in Angular, Typescript

Angular HttpClient JSON service minimal example

Using Angular’sHttpClient  in a service to fetch a JSON is pretty straightforward. In this example, we’ll fetch a JSON array.

Note that it’s recommended to create your own TypeScript interface for improved static typing, but nevertheless starting with any[] is often a good idea since it allows you to get started quickly.

public loadMyJSONArray(): Observable<any[]> {
  return this.http.get<any[]>(`/api/my-api`);
}

Full service example

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class MyService {

  constructor(private http: HttpClient) {
  }

  public loadMyJSONArray(): Observable<any[]> {
    return this.http.get<any[]>(`/api/my-api`);
  }

}

 

 

Posted by Uli Köhler in Angular, Typescript

Best practice Angular 14 production build command

This is the command I use to make production builds of Angular 14 webapps:
ng build ---aot --build-optimizer --common-chunk --vendor-chunk --named-chunks

While it will consume quite some CPU and RAM during the build, it will produce a highly efficient compiled output.

Posted by Uli Köhler in Angular, Javascript

Angular HttpClient plaintext (text/plain) minimal example

In order for Angular’s HttpClient to process plaintext responses and not result in an error, you need to set responseType: 'text' in the options(which is the second parameter to .get(). Otherwise, Angular will try to parse the plaintext response, even if the response MIME type is set to text/plain.

getPlaintext(command: string): Observable<string> {
  return this.http.get(`/api/text`, { responseType: 'text' });
}

Full service example

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PlaintextService {

  constructor(private http: HttpClient) { }
  
  getPlaintext(command: string): Observable<string> {
    return this.http.get(`/api/text`, { responseType: 'text' });
  }
}

 

Posted by Uli Köhler in Angular, Typescript

Angular HttpClient GET query parameter minimal example

sendCommand(command: string): Observable<any> {
  return this.http.get<any>(`/api/command`, {
    params: {"command": command}
  });
}

Running

sendCommand("test")

will send a HTTP GET request to /api/command?command=test.

Full service example

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CommandService {

  constructor(private http: HttpClient) { }

  sendSerialCommand(command: string): Observable<any> {
    return this.http.get<any>(`/api/command`, {
      params: {"command": command}
    });
  }

}

 

Posted by Uli Köhler in Angular, Typescript

How to fix error NG8002: Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’.

Problem:

When trying to load your Angular app in ng serve you see an error message like

error NG8002: Can't bind to 'ngModel' since it isn't a known property of 'input'.

2     <input type="text" pInputText [(ngModel)]="text"/>
                                    ~~~~~~~~~~~~~~~~~~~~~

Solution:

You have not loaded FormsModule in your app.module.ts.

Import it using

import {FormsModule} from '@angular/forms';

and load it by appending

FormsModule,

to imports: [...] in app.module.ts , for example:

imports: [
  BrowserModule,
  AppRoutingModule,
  HttpClientModule,
  InputTextModule,
  FormsModule,
],
Posted by Uli Köhler in Angular, Javascript

How to link Angular project dist directory to PlatformIO SPIFFS data directory

For tips how to make the Angular build small enough to fit into the SPIFFS image, see How to make Angular work with ESP32 SPIFFS / ESPAsyncWebserver

When you are building a PlatformIO image, you can easily make the dist/[project_name] directory from the Angular project directory appear in the SPIFFS image by using a symlink.

My config tells the server to serve from the www subdirectory.

server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("index.html");

Therefore, we first need to create the data directory in the same directory where platformio.ini is located:

mkdir data

Now we can create a symlink from the angular dist directory to data/www, for example:

ln -s ../MyUI/dist/myui data/www

PlatformIO will automatically handle the symlink, if the directory exists.

Posted by Uli Köhler in Angular, ESP8266/ESP32, PlatformIO

How to make Angular work with ESP32 SPIFFS / ESPAsyncWebserver

The main issue when using Angular web UIs is that the resulting files get too large, hence building the filesystem image will fail with SPIFFS_write error(-10001): File system is full.

Using these tips, I could get an Angular PrimeNG app to fit into a 4MB flash ESP32 module without any custom partition table and without any other crazy hacks! Even the fonts & PrimeNG icons fit into the SPIFFS easily, with a total of only 380 kB of the approximately 1.5 MB being consumed.

File compression

The number one most important tip is that you can just gzip -9 the files from the angular dist directory and ESPAsyncWebserver will automatically handle decompressing them!

This is my platformio.ini:

[env:esp32dev]
platform = espressif32
platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.3
board = esp32dev
framework = arduino
board_build.filesystem = littlefs
lib_deps =
    esphome/AsyncTCP-esphome@^1.2.2
    esphome/ESPAsyncWebServer-esphome@^2.1.0
    [email protected]
upload_speed = 460800
monitor_speed = 115200

This is my angular build script:

#!/bin/sh
ng build --aot --build-optimizer --optimization --progress --output-hashing none
gzip -9 dist/**/*

This is where I tell ESPAsyncWebserver (note that you should use the esphome fork) to serve files statically:

server.serveStatic("/", SPIFFS, "/www/").setDefaultFile("index.html");

Other tips

In order to make your life easier managing the data directory with both Angular files and other files, see How to link Angular project dist directory to PlatformIO SPIFFS data directory

You can use purgecss but compression works so well that it isn’t really worth both the risk of accidentally removing some CSS rules which you manually need to whitelist. Before discovering how well compression worked, I started to manually remove CSS rules from the PrimeNG theme file. This worked fine, but the SPIFFS still wasn’t small enough.

Often you can save space by deleting.

For example, primeicons.svg and primeicons.ttf are two different formats with the same content. Note that some (especially older, and some mobile) browsers don’t support all formats, hence it’s rather risky to remove them if you need to support multiple platforms.

Posted by Uli Köhler in Angular, ESP8266/ESP32, PlatformIO

How to remove hash from Angular “ng build” filenames

Angular generates filenames like Inter-Light.27083fa6375bb9ef.woff2 in the dist folder when building for production using ng build.

These hashes have the purpose of preventing the files from being cached, so if you remove the hash, you will need to find some other way of preventing caching

You can disable the hashes by using

--output-hashing none

as an argument to ng build.

Full ng build example:

ng build --aot --build-optimizer --optimization --progress --output-hashing none

 

Posted by Uli Köhler in Angular

How to use optional type in Angular / TypeScript

If you have a variable called ws of type WebSocket, you can not assign null or undefined to that variable

In order to make a variable that is either of the given type or undefined, use the following syntax:

ws?: WebSocket = undefined;

 

Posted by Uli Köhler in Angular, Javascript

How to configure Angular ‘ng serve’ API proxy

In order to proxy /api to http://localhost:62232 for example, first create proxy.conf.json in the same directory where package.json is located:

{
    "/api":
    {
        "target": "http://localhost:62232",
        "secure": false
    }
}

Now we need to modify package.json. Locate the line where ng serve is called, such as:

"start": "ng serve",

and add --proxy-config proxy.conf.json to the arguments of ng serve:

"start": "ng serve --proxy-config proxy.conf.json",

Full example for the scripts section of package.json:

"scripts": {
  "ng": "ng",
  "start": "ng serve --proxy-config proxy.conf.json",
  "build": "ng build --configuration=production",
  "watch": "ng build --watch --configuration development",
  "test": "ng test"
},

 

Posted by Uli Köhler in Angular, Javascript

Angular: ActivatedRoute minimal example

For this route:

{path: 'my/:id', component: MyDashboardComponent},

this is how you can use it in MyDashboardComponent:

constructor(private route: ActivatedRoute) {
      this.route.params.subscribe(params => {
          console.info(params.id);
      })
}

 

Posted by Uli Köhler in Angular, Javascript

How to fix Angular Service ngOnInit() not being called

Problem:

You have an Angular service implementing OnInit:

import { Injectable, OnInit } from '@angular/core';

@Injectable()
export class MyService implements OnInit {

  constructor() { }
  
  ngOnInit() {
    console.log("MyService initializing");
  }
}

but it never prints MyService initializing – i.e. the ngOnInit() function is never actually being called.

Solution:

Services should not implement OnInit, the function is deliberately never called. Instead, add the code from ngOnInit() in the constructor() and remove implements OnInit and ngOnInit():

import { Injectable, OnInit } from '@angular/core';

@Injectable()
export class MyService {

  constructor() {
    console.log("MyService initializing");
  }
  
}

 

Posted by Uli Köhler in Angular, Typescript

How to fix Angular HttpClient toPromise() deprecated (rxjs)

Problem:

You have angular HTTP client code like

this.http.get<MyType>(`${this.baseURL}/api/myAPI`).toPromise()

but toPromise() is deprecated in recent versions of angular / rxjs.

/** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */
toPromise(): Promise<T | undefined>;

Solution:

In most cases, for HttpClient, you want to use rxjs’s firstValueFrom() since the HttpClient Observables typically only return one value anyway.

First, import firstValueFrom from rxjs:

import { firstValueFrom } from 'rxjs';

then remove the .toPromise() call:

// Before
this.http.get<MyType>(`${this.baseURL}/api/myAPI`).toPromise()
// After
this.http.get<MyType>(`${this.baseURL}/api/myAPI`)

and surround the entire statement with firstValueFrom:

// Before
this.http.get<MyType>(`${this.baseURL}/api/myAPI`)
// After
firstValueFrom(this.http.get<MyType>(`${this.baseURL}/api/myAPI`).toPromise())

This will fix the issue.

Posted by Uli Köhler in Angular, Typescript

Angular HTTPClient ReplaySubject example without query parameters

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { MyType } from './my-type';

@Injectable()
export class MyService {
  baseURL = "http://localhost:18674";

  // Replay subject: New subscribers will get previous values
  public nodes = new ReplaySubject<MyType>();

  constructor(private http: HttpClient) {
    // Currently only acquire nodes once
    this.http.get<MyType>(`${this.baseURL}/api/myapi`).subscribe(value => 
      this.values.next(value);
    });
  }
}

 

Posted by Uli Köhler in Angular, Javascript, Typescript

How to fix Angular ng: not found

Problem:

When trying to run a command like ng build, e.g. in a CI setup, you see an error message like

sh: ng: not found

Preferred solution:

Only the machine where you’re running this command, you have not installed the ng command globally.

However, your node_modules folder will have a local installation of @angular/cli.

In order to use it, replace ng by ./node_modules/.bin/ng.

For example, a scripts section of your package.json

"scripts": {
  "build": "ng build"
},

would be replaced by

"scripts": {
  "build": "./node_modules/.bin/ng build"
},

Alternative solution:

Install ng globally on the machine running the command by using

sudo npm i -g @angular/cli

 

Posted by Uli Köhler in Angular

Best practice Angular 11 production build command

Note: This command does not work for Angular 14+ and is only kept for reference. See Best practice Angular 14 production build command for an updated command!
This is the command I use to make production builds of Angular 11+ webapps:
ng build --prod --aot --build-optimizer --common-chunk --vendor-chunk --named-chunks

 

While it will consume quite some CPU and RAM during the build, it will produce a highly efficient compiled output.

Posted by Uli Köhler in Angular, Javascript