Developers have the flexibility of several ways to bring Angular goodness to desktop apps.
Let’s talk Angular—one of the most popular modern web application development platforms. With a long history, matured tooling and rich developer ecosystem, it is not difficult to see why Angular is the SPA framework of choice for building modern web/mobile apps.
However, how does Angular work for desktop apps? The classic software answer applies—it depends. There are strategies for code sharing with web, but a lot also depends on the type of app being built and the desktop integrations desired.
PWAs and ElectronJS are established ways of bringing web code to desktop apps, and they are both good options for Angular. For .NET developers, .NET MAUI is the next-generation platform for building native cross-platform apps for mobile and desktop. And with Blazor hybrid apps, Blazor code for web apps is very welcome in .NET MAUI for mobile/desktop.
However, a lot of .NET shops in the past few years have been doing .NET in the backend and building SPA apps with JS on the frontend—can any of those investments come over to .NET MAUI land? Let’s talk about Angular, but the ways to enable desktop apps with web technologies should be about the same irrespective of the JS framework—so React, Vue and others should feel welcome too.
Angular goodness on desktop—let’s do this.
This post was written and published as part of the 2021 C# Advent.
If you’re new to the Angular world, one of the best ways to get started is the Angular CLI. The prerequisites to building with Angular are an LTS Node.js version for the runtime and npm for dependencies. It’s easy to globally install the Angular CLI tool, like so:
npm install -g @angular/cli
We can then fire up a new Angular app—the template walks you through a few settings before creating the project.
ng new angularcli
The Angular CLI tool installs the necessary Angular npm packages and other dependencies—once done, we can navigate inside the project and see all the code/configurations.
cd angularcli
Ready to run your Angular app locally? The Serve command compiles the app in memory, launches the server and watches local files for deploying changed components of the app.
ng serve --open
Voilà. That’s how easy it is to start making modern web apps with Angular. And while you’re getting started, it may be worth getting some UI ammunition to deliver good UX and performance—Kendo UI for Angular can help.
Kendo UI for Angular delivers components to meet app requirements for data handling, performance, UX, design, accessibility and so much more—100+ fully native components for building high-quality modern Angular UI in no time.
Now that we have a basic Angular web app running, let’s talk about options to get that Angular goodness on desktop apps.
One of the easiest ways for a web app to work on desktop is PWA—web apps can be progressively better citizens on mobile/desktop platforms. PWAs are essentially web apps, but with hooks to have native-like features—be installable on desktop and have service workers bring in offline support, push notifications, hardware access and more. It is pretty easy to start turning a regular Angular web app into a PWA—just a simple command:
ng add @angular/pwa
This actually touches a few things in an existing Angular app—a new manifest file is dropped in that tells the browser how the app should behave when installed by the user. The starter set has a variety of app icons for pinning to home screen, Touchbar and more—the default icons are added to an Asset's directory.
{
"name": "angularpwa",
"short_name": "angularpwa",
"theme_color": "#1976d2",
"background_color": "#fafafa",
"display": "standalone",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
...
...
]
}
The startup index.html page now has a reference to the new web manifest file.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angularpwa</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
</head>
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>
A default caching service worker is dropped in as well, with configuration file namely ngsw-config.json—this indicates which types of assets can be cached.
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}
}
]
}
The ServiceWorker config file needs to be referenced in the angular.json file—the ngswConfigPath links the ServiceWorker, enabling production configuration in build schematics.
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angularpwa": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angularpwa",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles.css"
],
"scripts": [],
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"serviceWorker": true,
"ngswConfigPath": "ngsw-config.json"
},
...
...
}
}
}
}
}
}
And finally, the app.module.ts now imports in ServiceWorkerModule and registers the ServiceWorker.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
registrationStrategy: 'registerWhenStable:30000'
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
That’s a run-down of all the moving pieces that start transforming a regular Angular app into a PWA. However, we cannot just run the app with Angular Serve command—service workers do not work with in-memory compilations. We need a production build, like so:
ng build --prod
Once the build is done with Production configuration, we can see the artifacts in the Distribution directory—everything is compiled down to deployable plain HTML/JS/CSS.
cd dist/angularpwa
Next up, we need a small HTTP sever to expose the built files out to the browser as apps and fire things up.
npm i -g http-server
http-server -p 8080 -c-1
Excellent—we can navigate to local HTTP server IP and see our PWA in action! Notice the install option, which indicates that the user can install and run this app on desktop without the browser chrome.
If we open up Developer Tools on our browser, we can also see the default caching Service Worker being registered and running. This is just the start—developers can now start adding all the customizations to make a nice modern PWA with Angular.
Another strong contender for bringing Angular apps to desktop is Electron—the popular open-source battle-tested way of hosting web apps on desktop.
Electron helps in building robust cross-platform desktop apps that are essentially HTML/CSS/JS, but with strong integrations with the host OS when running as a desktop app. Electron does pack two things to provide a consistent runtime and rendering canvas—Node.js and the Chromium engine.
Starting from a standard Angular CLI app, it is not difficult to add Electron support—let’s bring in the Electron package as a dev dependency.
npm install electron@latest --save-dev
Now, to configure all things Electron inside an Angular project, let’s add a simple App.js file. To bootstrap Electron to run on desktop, we essentially need to new-up a browser window with specified dimensions and load our Angular app’s URL. We also need listeners for when the app windows open/close—here’s the code:
const {
app,
BrowserWindow
} = require('electron')
const url = require("url");
const path = require("path");
let appWindow
function initWindow() {
appWindow = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
nodeIntegration: true
}
})
appWindow.loadURL(
url.format({
pathname: path.join(__dirname, `/dist/index.html`),
protocol: "file:",
slashes: true
})
);
appWindow.on('closed', function () {
appWindow = null
})
}
app.on('ready', initWindow)
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
if (win === null) {
initWindow()
}
})
Next up, we need to configure our Angular app to build itself and Bootstrap from the App.js file—here is the setup in package.json with the main entry point and a build script to start Electron:
{
"name": "angularelectron",
"version": "0.0.0",
"main": "app.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"start:electron": "ng build --base-href ./ && electron ."
},
...
...
}
That’s all—now we can fire up our app, and see Angular hosted within the Electron shell.
npm run start:electron
We have a full Chromium engine embedded inside the app—so we could do things like opening up Chrome DevTools from code:
appWindow.webContents.openDevTools()
.NET MAUI is the evolution of Xamarin.Forms and represents a modern cross-platform .NET solution to reach mobile and desktop platforms. Blazor is very welcome in .NET MAUI—essentially .NET MAUI bootstrapping the app and Blazor code rendering within the BlazorWebView. This is obviously a good story for app modernization—Blazor and .NET MAUI both run on .NET 6 and developers can now share code between web/desktop.
But what if you already had investments in JavaScript? What if you were already building modern web apps with JS SPA frameworks? Would any of that be brought over to .NET MAUI? Let’s take a look at the Angular story.
As we’ve seen getting started with the Angular CLI, Angular web apps do depend on Node.js and Node modules—which is not something we have with .NET MAUI running on .NET 6. However, let’s compile our Angular CLI app, like so:
ng build --prod
If we look in the dist folder, the artifacts of the build are pure web assets—all the TypeScript, Node dependencies and other things get compiled down to basic HTML/JS/CSS.
If we take a look at the index.html file, it simply references all the JavaScript and knows how to display the root app component.
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>Angularcli</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.ef46db3751d8e999.css"></head>
<body>
<app-root></app-root>
<script src="runtime.8711a0b48f514fd6.js" type="module"></script><script src="polyfills.24f5ee6314fed4d1.js" type="module"></script><script src="main.756852958de70a14.js" type="module"></script>
</body></html>
Let us start a .NET MAUI project with the Maui-Blazor template—essentially a hybrid app with full native capabilities, but with Blazor UI rendered through a modern WebView for mobile and desktop platforms.
However, could we swap out Blazor with Angular? They’re both modern web frameworks rendering UI for the browser, but with different runtimes—maybe the WebView will not care? Let’s bring the compiled Angular CLI app files into our .NET MAUI-Blazor app and drop them in the wwwroot directory as static files, like so:
The index.html files acts as the starting point for the Blazor inside .NET MAUI app—what if we replace it with the one we get from the Angular app? Let’s go look in the MainPage.xaml file:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
xmlns:local="clr-namespace:MauiAngular"
x:Class="MauiAngular.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<b:BlazorWebView HostPage="wwwroot/index.html">
<!-- <b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
</b:BlazorWebView.RootComponents> -->
</b:BlazorWebView>
</ContentPage>
This is where the rubber meets the road—the .NET MAUI app throws up the big BlazorWebView component for Blazor to do its thing. BlazorWebView is essentially a wrapper—rendering Webiew2 on Windows, WKWebView on macOS or whichever is the latest relevant WebView component based on the platform where the app is running on.
Here in the MainPage.xaml, we can still point it to go render the wwwroot/index.html file, but we have it swapped with the Angular file now. And we stop Blazor from rendering its base Root component—so it should be all Angular driving the UI within the WebView. We hesitantly do a .NET MAUI build:
dotnet build -t:Run -f net6.0-ios
Voilà—we get the Angular CLI app now running inside of .NET MAUI on iOS!
Let’s do a desktop build:
dotnet build -t:Run -f net6.0-maccatalyst
Works on macOS, and Windows should be no different.
Since we fed the BlazorWebView with HTML/CSS/JS, it did not care that we did not render any Blazor UI—web UI is web UI. And now it is welcome inside .NET MAUI cross-platform apps. Angular code would need a JS Interop to talk to .NET code, but the promise to bring Angular to .NET desktop/mobile apps is just starting.
Angular is wonderful and enables developers to build modern web apps. But much of the Angular goodness—components, data-binding, routing and more, can be brought over to desktop apps. While PWAs and Electron are the present reality, .NET MAUI represents the promise of tomorrow to bring Angular to Windows/macOS desktop. Let’s reuse code!
Sam Basu is a technologist, author, speaker, Microsoft MVP, gadget-lover and Progress Developer Advocate for Telerik products. With a long developer background, he now spends much of his time advocating modern web/mobile/cloud development platforms on Microsoft/Telerik technology stacks. His spare times call for travel, fast cars, cricket and culinary adventures with the family. You can find him on the internet.