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.
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, ],
In our previous post EMQX 5.x HTTP password authentication server minimal example using NodeJS we provided a complete example of how to implement EMQX HTTP authentication.
This post provides an extension to our previous HTTP auth by adding a Koa router (i.e. a HTTP endpoint / URL) to provide ACL authentication, i.e. allow or deny topic level access with custom logic.
router.post('/emqx/acl', async ctx => { const body = ctx.request.body; console.log(body) // TODO: This example always returns true // You need to implement your authentication logic ctx.body = { result: "allow", }; });
Add that code before app.use(router.routes());
in the script from EMQX 5.x HTTP password authentication server minimal example using NodeJS.
My recommended Authorization configuration body which you can set in the EMQX dashboard is
{ "action": "${action}", "client_id": "${clientid}", "ip": "${peerhost}", "topic": "${topic}", "username": "${username}" }
Note: If you also want to implement ACL authorization via HTTP, see our extension to this example: EMQX 5.x HTTP ACL server using NodeJS
This server implements a minimal HTTP authentication server. In this minimal example, it will always allow authentication – you need to implement your own logic to verify passwords. Note: This server is written for EMQX versionx5.0 and will not work for EMQX 4.x without modification. See the official documentation on EMQX 5.x HTTP auth for more information.
#!/usr/bin/env node const router = require('koa-router')(); const koaBody = require('koa-body'); const Koa = require('koa'); const app = new Koa(); app.use(koaBody()); router.post('/emqx/auth', async ctx => { const body = ctx.request.body; const username = body.username; const password = body.password; // TODO: This example always returns "allow" // You need to implement your authentication logic ctx.body = { result: "allow", is_superuser: false }; }); app.use(router.routes()); if (!module.parent) app.listen(19261);
This script is based on our previous post Minimal Koa.JS example with Router & Body parser, hence you can install the dependencies using
npm i --save koa koa-router koa-body
The request body config which you can set in the EMQX dashboard is
{ "client_id": "${clientid}", "password": "${password}", "username": "${username}" }
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" },
Use this code in Javascript:
APP.conference.listMembers()
If no members are present, this will simply be an empty list:
> APP.conference.listMembers() []
But if there is another participant, it will show some info:
> APP.conference.listMembers() [ci] 0: ci {_jid: '[email protected]/b009ae10', _id: 'b009ae10', _conference: nd, _displayName: 'Foo', _supportsDTMF: false, …} length: 1
This code clicks the join button:
document.querySelector(".prejoin-preview-dropdown-container div[role='button']").click()
By using the correct @match
, you can activate this only for a specific conference:
Complete script:
// ==UserScript== // @name AutojoinJitsiMeet // @namespace http://tampermonkey.net/ // @version 0.1 // @description ? // @author You // @match https://meet.jit.si/TestAutoJoin // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net // @grant none // ==/UserScript== (function() { 'use strict'; document.querySelector(".prejoin-preview-dropdown-container div[role='button']").click() })();
First install ts-node-dev:
npm i --save-dev ts-node-dev
Then add
"start": "./node_modules/.bin/tsnd --respawn app.ts"
to package.json
in the scripts
section, example:
{ "name": "myapp-backend", "version": "1.0.0", "description": "To make it easy for you to get started with GitLab, here's a list of recommended next steps.", "main": "index.js", "scripts": { "build": "./node_modules/.bin/tsc -b tsconfig.json", "start": "./node_modules/.bin/tsnd --respawn app.ts" } /* ... */ }
See the dayjs docs for more info about the library.
const now = dayjs(); const nextMonth = now.month(now.month() + 1).date(1).hour(0).minute(0).second(0); // Example usage console.info(nextMonth.format()) // Prints, for example, "2022-03-01T00:00:00+01:00"
This will also work in december as DayJS processes month(13)
correctly:
const now = dayjs('2021-12-09'); const nextMonth = now.month(now.month() + 1).date(1).hour(0).minute(0).second(0); // This prints '2022-01-01T00:00:00+01:00' console.info(nextMonth.format());
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); }) }
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.
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"); } }
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>;
In most cases, for HttpClient, you want to use rxjs’s firstValueFrom()
since the HttpClient Observable
s 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.
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); }); } }
const mqtt = require('mqtt') const client = mqtt.connect('mqtt://user:[email protected]') client.on('connect', () => { client.subscribe('mytopic'); }) client.on('message', (topic, message) => { console.log(topic, JSON.parse(message)) })
If required, you can install the mqtt
library using
npm i --save mqtt