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/[email protected]^1.2.2
    esphome/[email protected]^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

How to get Jitsi meet list of participants using Tapermonkey script

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

 

Posted by Uli Köhler in Audio/Video, Javascript

How to autojoin Jitsi Meet meeting using Tapermonkey script

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()
})();

 

Posted by Uli Köhler in Audio/Video, Javascript

NodeJS typescript simple auto-reload on file change

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"
  }
  /* ... */
}

 

Posted by Uli Köhler in Javascript, NodeJS

How to get first day of next month in Javascript using DayJS

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());

 

Posted by Uli Köhler in 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

NodeJS MQTT minimal subscribe example with JSON messages

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

 

Posted by Uli Köhler in Javascript, MQTT, NodeJS

NodeJS MQTT minimal subscribe example

Also see: NodeJS MQTT minimal subscribe example with JSON messages

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, message)
})

If required, you can install the mqtt library using

npm i --save mqtt

 

Posted by Uli Köhler in Javascript, MQTT, NodeJS

How to compute SHA hash of local file in Javascript using SubtleCrypto API

The following example uses the SubtleCrypto API to compute the SHA-256 hash of a file that is selected in a file input. The checksum is computed entirely on the client side, the file does not need to be uploaded to a server at all.

This code has been verified to generate the same checksum as if running sha256sum on the command line.

Full example

You can download this file, save it as index.html and open it in the browser. Then select a file and check the develper

<html>
<body>
    <input type="file" id="myfile" onchange="onMyfileChange(this)" />

    <script src="https://unpkg.com/[email protected]/dist/jszip.js" type="text/javascript"></script>
    <script type="text/javascript">
        function onMyfileChange(fileInput) {
            if(fileInput.files[0] == undefined) {
                return ;
            }

            var filename = fileInput.files[0].name;
            // var filesize = fileInput.files[0].size;
            var reader = new FileReader();
            reader.onload = function(ev) {
                console.log("File", filename, ":");
                // 
                crypto.subtle.digest('SHA-256', ev.target.result).then(hashBuffer => {
                    // Convert hex to hash, see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
                    const hashArray = Array.from(new Uint8Array(hashBuffer));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
                    console.log(hashHex);
                }).catch(ex => console.error(ex));
            };
            reader.onerror = function(err) {
                console.error("Failed to read file", err);
            }
            reader.readAsArrayBuffer(fileInput.files[0]);
        }
    </script>
</body>
</html>
Posted by Uli Köhler in Javascript

How to get page HTML source code in Puppeteer

In order to get the current page HTML source code (i.e. not the source code received from the server, but the currently loaded source code including Javascript modifications), use

await page.content()

Full example based on Puppeteer minimal example:

// Minimal puppeteer get page HTML source code example
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://techoverflow.net', {waitUntil: 'domcontentloaded'});
  // Wait for 5 seconds
  console.log(await page.content());
  // Take screenshot
  await browser.close();
})();

 

Posted by Uli Köhler in Javascript, NodeJS, Puppeteer

How to sleep for X seconds in Puppeteer

In order to sleep for 5 seconds, use

await page.waitForTimeout(5000);

Full example based on Puppeteer minimal example:

// Minimal puppeteer example
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://techoverflow.net', {waitUntil: 'domcontentloaded'});
  // Wait for 5 seconds
  await page.waitForTimeout(5000);
  // Take screenshot
  await page.screenshot({path: 'screenshot.png'});
  await browser.close();
})();

 

Posted by Uli Köhler in Javascript, NodeJS, Puppeteer

How to install NodeJS 16.x LTS on Ubuntu in 1 minute

Run these shell commands on your Ubuntu computer to install NodeJS 14.x:

sudo apt -y install curl
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs

Instead of setup_16.x you can also choose other versions like setup_14.x. However, using this method, you can’t install multiple versions of NodeJS in parallel.

Source: Official nodesource documentation

Posted by Uli Köhler in Linux, NodeJS

How to identify the latest npm package version

You can use

npm info [package name] version | head -n1

to print just the latest package version, for example:

npm info @angular/cli version | head -n1

The head -n1 part is neccessary to suppress unwanted npm info messages. Just running the command without head -n1 :

npm info @angular/cli version

prints the following output:

12.2.6
npm notice 
npm notice New minor version of npm available! 7.21.1 -> 7.24.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v7.24.0
npm notice Run npm install -g [email protected] to update!
npm notice

I typically use the node:latest docker container instead of a local npm to find the latest version of an npm package:

docker run -it --rm node:latest npm info meshcommander version | head -n1

At the time of writing this article, the command prints

0.9.0-d

 

Posted by Uli Köhler in NodeJS