<app-my-component #myId></app-dynamic-plot>
@ViewChild('myid', {read: MyComponent}) myComponent!: MyComponent;
<app-my-component #myId></app-dynamic-plot>
@ViewChild('myid', {read: MyComponent}) myComponent!: MyComponent;
Use [feedback]="false"
to hide the strength meter:
<p-password [feedback]="false" [(ngModel)]="value"></p-password>
On my Desktop, Angular commands such as
ng build
or
ng new
worked successfully, but they didnt stop / terminate after running the command. Instead, they just waited indefinitely without any output.
This is related to analytics being enabled, see this GitHub post.
In order to fix it, run
ng analytics disable --global
It will print
Global setting: disabled Local setting: enabled Effective status: disabled
and it won’t terminate the first time you do this. Just press Ctrl+C
to force stop it.
After that, any command will exit after finishing its task
I suspect this has something to do with IPv6 configuration since my Desktop’s IPv6 network setup is somewhat broken at the moment. However, I did not do any further research into this.
When opening your Angular app with a standalone component, you see the following error message:
Can't bind to 'routerLink' since it isn't a known property of 'a'
In the component where the error occurs, add
import { RouterModule } from '@angular/router';
and add
RouterModule,
to the imports
of the component. Example:
@Component({ selector: 'app-top-bar', standalone: true, imports: [ CommonModule, RouterModule ], templateUrl: './top-bar.component.html', styleUrl: './top-bar.component.sass' }) export class TopBarComponent { }
You see the following error message while compiling your angular application:
Error: src/app/my/my.component.ts:9:4 - error TS2552: Cannot find name 'Input'. Did you mean 'oninput'?
Open your module file, e.g. my.component.ts
and add the following line at the top:
import { Input } from '@angular/core';
If you see an error message such as
Error: src/app/my-component/my-component.component.html:2:1 - error NG8002: Can't bind to 'routerLink' since it isn't a known property of 'p-button'. 1. If 'p-button' is an Angular component and it has 'routerLink' input, then verify that it is part of this module. 2. If 'p-button' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.
in your angular app, open the module file that is declaring your component (typically app.module.ts
) and add
RouterModule,
to its imports
, for example:
imports: [ CommonModule, RouterModule, ]
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)
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>
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.
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`); })); } }
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
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`); }
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`); } }
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.
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' }); }
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' }); } }
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
.
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} }); } }
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"/> ~~~~~~~~~~~~~~~~~~~~~
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, ],
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.
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.
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");
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.
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
.
ng build
example:ng build --aot --build-optimizer --optimization --progress --output-hashing none
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;
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" },