Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / ECMAScript

ASP.NET Core SPA with Preact and HTM

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
27 Jan 2023MIT21 min read 7.7K   5   2
Create lightweight VS 2022 ASP.NET Core SPA template with React style components
This is a tutorial to show how to create a lightweight Visual Studio 2022 ASP.NET Core SPA template with React style components supporting type checking, shared models, bundling and minification but no compile round.

Introduction

Few years ago, I needed a lightweight solution for small ASP.NET (Core) projects. My requirements were:

  • Development done in full blown Visual Studio
  • Type checking
  • Reusable components
  • ECMAScript module support
  • No compile or very fast compile
  • Bundling and minification for production

I tried Visual Studio included React templates with typescript and while it fulfilled most of my criteria not to mention access to all kind of fancy React libraries I could not bear the compile time needed to compile. And it was not lightweight at all. I tried number of other templates as well, for example Blazor which was not so mature at the time. It also was not at all lightweight, seemed to need quite a learning curve and creating bindings to JavaScript libraries I needed would have required time I could not bear. I also played with Visual Studio Code and while it would have greatly accelerated front end coding compared to full Visual Studio, I could not trade out C# of full Visual Studio. Also I did not want to break solution to multiple projects in multiple editors. Then I found great library Preact, a 3kb React clone with same API. They also had their own alternative to JSX, HTM (Hyperscript Tagged Markup), which runs directly in browser, hence precompilation is not needed. In this tutorial project template similar to Visual Studio’s React template is created step by step with ASP.NET Core, Preact, HTM and some other help libraries. Bootstrap will be uses for CSS as in original React template.

Here, I could also mention that prior to this template, I never tried to make anything like this myself but used only ready baked templates included in Visual Studio. I had checked what was inside, for example, Visual Studio's React template but didn't understand single line of it nor the tools used. That changed when I gave myself few days to dig into modern web development basics so this is also some kind of tutorial to those tools.

Result of this tutorial can also be downloaded from GitHub if you are in a hurry.

Creating Back End

Let’s start by creating a new project and choosing ASP.NET Core Web App and name it AspNetCorePreactHtm with default settings. As we will use JavaScript to create DOM content, open Pages\Shared_Layout.cshtml and replace content with:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AspNetCorePreactHtm</title>        
</head>
<body>
    @RenderBody()    
    @RenderSection("Scripts", required: false)
</body>
</html>

Open Pages\index.cshtml and replace content with:

HTML
@page 

If you run the project, a blank page should open as there’s no visible html.

Creating Preact Front End with Virtual DOM that Compiles at Browser

First, delete all existing content in wwwroot/lib in solution explorer. Then right click empty wwwroot/lib folder and choose Add / Client-Side Library. In opening window, type bootstrap in library textbox and hit enter. Latest Bootstrap version appears. Choose Choose specific files and select only css/bootstrap.min.css and js/bootstrap.bundle.min.js and click Install. Right click wwwroot/lib folder and choose again Add Client-Side Library. In opening window, type htm in library textbox and hit enter. Latest HTM library version appears. Choose Choose specific files and select only preact/standalone.module.js and click Install. Standalone module contains both Preact and HTM in one package. Right click wwwroot and choose Add / New Folder and name it src. Right click wwroot/src and choose Add / New Item / JavaScript file and name it App.js. Open created file wwwroot/src/App.js and paste the following code:

JavaScript
import { html, Component, render } from '../lib/htm/preact/standalone.module.js';

class App extends Component {

    constructor() {
        super();
    }

    render() {
        return html`Hello World!`;
    }
}

render(html`<${App} />`, document.body);

First line imports necessary JavaScript for Preact and HTM. Then follows Preact component that simply renders text "Hello world". Last line then tells Preact to render component to html document's body part.

Open again, _Layout.cshtml and replace content with the following snippet:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AspNetCorePreactHtm</title>    
    <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
    <script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script defer type="module" src="~/src/App.js"></script>    
</head>
<body>
    @RenderBody()
    @RenderSection("Scripts", required: false)
</body>
</html>

One line has been added and that is reference to App.js we just created. It has important definition, defer type="module", which tells browser that code contains modern ECMAScript code instead of common JavaScript. Run application again and hopefully, you will see Hello world in opening web page. Preact's virtual DOM engine is wired up!

Adding WeatherForecast API to Back End

In Razor page applications, http APIs are defined by services. Next, we’ll create API that will return weather forecast like in default React template. Right click AspNetCorePreactHtm application in solution explorer and choose Add / New Folder to root and name it Shared. Right click new Shared folder and choose Add / New Item / Code File and name it WeatherForecastSummary.cs. Paste the following code:

C#
namespace AspNetCorePreactHtm.Shared
{
    /// <summary>
    /// Forecast feel enum definition
    /// </summary>
    public enum WeatherForecastSummary
    {
        /// <summary>
        /// Feels freezing
        /// </summary>
        Freezing,
        
        /// <summary>
        /// Feels bracing
        /// </summary>
        Bracing, 
        
        /// <summary>
        /// Feels chilly
        /// </summary>
        Chilly, 
        
        /// <summary>
        /// Feels cool
        /// </summary>
        Cool, 
        
        /// <summary>
        /// Feels mild
        /// </summary>
        Mild, 
        
        /// <summary>
        /// Feels warm
        /// </summary>
        Warm, 
        
        /// <summary>
        /// Feels balmy
        /// </summary>
        Balmy, 
        
        /// <summary>
        /// Feels hot
        /// </summary>
        Hot, 
        
        /// <summary>
        /// Feels sweltering
        /// </summary>
        Sweltering, 
        
        /// <summary>
        /// Feels scorching
        /// </summary>
        Scorching
    } 
}

Add another code File to Shared folder and name it WeatherForecast.cs and paste the following code:

C#
using System.ComponentModel.DataAnnotations;

namespace AspNetCorePreactHtm.Shared
{
    /// <summary>
    /// Weather forecast class definition
    /// </summary>
    public class WeatherForecast
    {
        /// <summary>
        /// Forecast date time
        /// </summary>
        public DateTime Date { get; set; } = DateTime.Now;

        [Range(-50, 100)]
        /// <summary>
        /// Forecast tempereture in celsius degrees
        /// </summary>
        public int TemperatureC { get; set; } = 0;

        [Range(-58, 212)]
        /// <summary>
        /// Forecast temperature in Fahrenheit degrees
        /// </summary>
        public int TemperatureF { get; set; } = 32;

        /// <summary>
        /// Forecast summary enum value
        /// </summary>
        public WeatherForecastSummary Summary 
               { get; set; } = WeatherForecastSummary.Cool;
    }
}

There are a lot of comments we don’t need right now but we’ll come back to that later. Right click project as Solution Explorer and choose Add / New Item / Code File and name it WeatherForecastService.cs. Paste the following code:

C#
namespace AspNetCorePreactHtm
{
    using AspNetCorePreactHtm.Shared;

    public interface IWeatherForecastService
    {
        string Get();       
    }
    public class WeatherForecastService : IWeatherForecastService
    {       
        public string Get()
        {
            var WeatherForecasts = new List<WeatherForecast>();
            for (int i = 1; i <= 5; i++)
            {
                WeatherForecast wf = new WeatherForecast();
                wf.Date = DateTime.Now.AddDays(i);
                wf.TemperatureC = Random.Shared.Next(-20, 55);
                wf.TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556);
                wf.Summary = (WeatherForecastSummary)Random.Shared.Next
                (0, Enum.GetNames(typeof(WeatherForecastSummary)).Length-1);

                WeatherForecasts.Add(wf);
            }
            return System.Text.Json.JsonSerializer.Serialize(WeatherForecasts);
        }        
    }
}

This defines our service of weather forecasts. To make it effective, we need to register it to web application builder. Open Program.cs and just after line builder.Services.AddRazorPages(); paste the following snippet:

JavaScript
// register weatherforecast service as singleton
builder.Services.AddSingleton<AspNetCorePreactHtm.IWeatherForecastService, 
                 AspNetCorePreactHtm.WeatherForecastService>();

We still need to define relative path for http GET request that responds with our new service. Just before the last line of file app.Run(); paste the following snippet:

JavaScript
// map weatherforecast api
app.MapGet("/api/weatherforecast", 
    (AspNetCorePreactHtm.IWeatherForecastService service) =>
{
    return Results.Ok(service.Get());
});

Now WeatherForeCast API is complete. You may notice that created WeatherForecast API returns forecasts as json string instead of returning an array. Reason for this is that service first serializes send data to JSON. Default serialization converts property name to camel case (first letter always small). Calling System.Text.Json.JsonSerializer.Serialize directly maintains Pascal case and property names will remain.

Creating Front End SPA

Now it’s time to create our actual front end SPA (Single Page Application). Each page will be defined in a separate file.

Home Page

Let’s start with the home page. Right click wwroot/src and choose Add / New Item / JavaScript file and name it Home.js. Paste the following code:

JavaScript
"use strict";

import { html, Component } from '../lib/htm/preact/standalone.module.js';

export class Home extends Component {

    constructor(props) {
        super(props);
    }

    render() {
        return html`
 <div>
    <h1>Hello, world!</h1>
    <p>Welcome to your new single-page application, built with:</p>
    <ul>
        <li><a href='https://get.asp.net/'>ASP.NET Core</a> and 
            <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx' 
             target="_blank" rel="noopener noreferrer">C#</a> 
             for cross-platform server-side code</li>
        <li><a href='https://preactjs.com/' target="_blank" 
             rel="noopener noreferrer">Preact</a> with 
            <a href='https://github.com/developit/htm'>HTM 
            (Hyperscript Tagged Markup)</a> rendering for client-side code</li>
        <li><a href='http://getbootstrap.com/' target="_blank" 
             rel="noopener noreferrer">Bootstrap</a> for layout and styling</li>
    </ul>
    <p>To help you get started, we have also set up:</p>
    <ul>
        <li><strong>Client-side navigation</strong>. 
        For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
    </ul>    
 </div>
      `;
    }
}

Home component is a plain component and renders just static HTML. Option use strict at start of file will force us to write cleaner code, declare all used variables, etc.

Counter Page

Next we’ll create counter page. Right click wwroot/src and choose Add / New Item / JavaScript file and name it Counter.js. Paste the following code:

JavaScript
"use strict";

import { html, Component } from '../lib/htm/preact/standalone.module.js'; 

export class Counter extends Component {

    constructor(props) {
        super(props);
        this.state = { currentCount: 0 };
    }

    incrementCounter() {
        this.setState({
            currentCount: this.state.currentCount + 1
        });
    }

    render() {
        return html`
<div>
    <h1>Counter</h1>
    <p>This is a simple example of a React component.</p>
    <p aria-live="polite">Current count: <strong>${this.state.currentCount}</strong></p>
    <button class="btn btn-primary" onClick=${() => 
            this.incrementCounter()}>Increment</button>
</div>
      `;
    }
}

Counter component demonstrates how to wire DOM element to your own function inside template literal. Notice how state's value currentCount is fetched on render as well as increment button's onClick event is wired to component's internal incrementCounter() function.

FetchData Page

Right click wwroot/src and choose Add / New Item / JavaScript file and name it FetchData.js. Paste the following code:

JavaScript
"use strict";

import { html, Component } from '../lib/htm/preact/standalone.module.js';

var feelsLike = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", 
                 "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

export class FetchData extends Component {

    constructor(props) {
        super(props);
        this.state = { forecasts: [], loading: true };
    }

    componentDidMount() {
        this.populateWeatherData();
    }

    async populateWeatherData() {
        const response = await fetch('api/weatherforecast');
        const json = await response.json();
        this.state.forecasts = JSON.parse(json);
        this.state.loading = false;
        this.forceUpdate();
    }

    render() {
        if (this.state.loading) {
            return html`<p><em>Loading...</em></p>`;
        }
        else {
            return html`
<div>
    <h1>Weather forecast</h1>
    <p>This component demonstrates fetching data from the server.</p>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
    ${this.state.forecasts.map(f => html`
            <tr>
                <th scope="row">${f.Date.toLocaleString()}</th>
                <td>${f.TemperatureC}</td>            
                <td>${f.TemperatureF}</td>
                <td>${feelsLike[f.Summary]}</td>
            </tr>
    `)}
        </tbody>
    </table>    
</div>
`;
        }
    }
}

FetchData component demonstrates how to render list inside template literal with JavaScript map method. Fetching data by http call from server may be a lengthy process and therefore async function, that triggers render when fetch is done, is used for it.

Main Component

Now all pages are defined and it’s time to update application's main component, App component, so that it will route to and render created page components. Open wwwroot/src/App.js and replace the existing code with:

JavaScript
"use strict";

import { html, Component, render } from '../lib/htm/preact/standalone.module.js';
import { Home } from './Home.js';
import { Counter } from './Counter.js'
import { FetchData } from './FetchData.js'

// router pages, first page is considered home page
var pages = { '#Home': Home, '#Counter': Counter, '#FetchData': FetchData };

class App extends Component {

    constructor() {
        super();

        // window back navigation handler
        window.onpopstate = () => { this.Navigate(null); };

        // initial page
        this.state = { navPage: '#Home' };
    }
    
    Navigate(toPage) {        
        this.setState({ navPage: toPage });        
    }

    render() {

        // get page to navigate to or browser back/forward navigation page or
        // first (home) page
        let page = this.state.navPage ? this.state.navPage : 
        window.location.hash ? window.location.hash : Object.entries(pages)[0][0]; 
        
        // push page to browser navigation history if not current one
        if (window.location.hash !== page) {
            window.history.pushState({}, page, window.location.origin + page);
        }                   
          
        let content = html`<${pages[page]} />`;

        return html`
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" style="cursor: pointer" 
     onClick=${() => this.Navigate("#Home")}>PreactHtmStarter</a>
    <button class="navbar-toggler" type="button" 
     data-bs-toggle="collapse" data-bs-target="#navbarNav" 
     aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" style="cursor: pointer" 
           onClick=${() => this.Navigate("#Home")}>Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" style="cursor: pointer" 
           onClick=${() => this.Navigate("#Counter")}>Counter</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" style="cursor: pointer" 
           onClick=${() => this.Navigate("#FetchData")}>Fetch data</a>
        </li>        
      </ul>
    </div>
  </div>  
</nav>
<div class="container-fluid body-content">
    ${content}
</div>
      `;
    }
}

render(html`<${App} />`, document.body);

Lot has changed in App.js but we'll dig into it a little later. Now run the project. Web page is similar to default React template opens. Use navigation bar links to browse between pages. Counter page count button increments count on page and Fetch Data page shows random weather forecasts from http api we created earlier. Browser’s back and forward buttons follow history of our earlier navigations within our single page application.

So How Does It All Work?

Pages

In React's or Preact's perspective, everything is a component: App is our main component and Home, Counter and FetchData are the page components. Because App component imports page components from other JavaScript files they must be exported in their source code, for example, export class Counter extends Component in Counter.js. Each component has render function that renders its contents to screen. A component can contain other components like App component contains all page components.

Router

Typically React and Preact use some external router library but here router is embedded in App component by super simple hash router implementation, which uses browser window history api to store navigations. It simply consist of dictionary containing page name as key and page component as value. # character on name of each page tells browser to use JavaScript API instead of fetching data from server. When App component is created, constructor function is called first and there window back navigation handler function is defined. Also html navbar links do not contain any href link but mimics one by changing cursor on hover and wiring onClick event to our own navigation function with page name as parameter. Navigate function call then uses Preact component’s SetState function which both mutates component’s internal state by merging any value in SetState call to components internal state and triggers render to update view, in this case, navigated page component. When navigating to page, its name and virtual path are pushed to browsers navigation history to be popped later. Render function then decides page to route: if state navPage is null page name is fetched from browser history. Page component is then red from dictionary and rendered to content section of App component. This is not my invention but from vanilla JavaScript web article I read years ago. I just ported it to Preact component. I'd really like to give credit to the original author, but have no recollection who it was.

Template Literals

Template literals look much like JSX templating language that React uses. JSX must be precompiled to JavaScript React nodes for browser to understand them. On the other hand, HTM library uses template literals to transpile them to Preact virtual DOM nodes in browser so precompile step can be skipped. While JSX cannot use some reserved words html like class and uses className instead template literals don't have this restriction. Transpiling is also possible to do at server side but we’ll get back to that on minification.

Generic Type Checking

Now template is already fully functional. If project using it is kept small, this might just be enough. JavaScript is by nature non-typed language which means that any variable can have any value at all at any time and that is fine with JavaScript and cannot spot misspellings, etc. at editor. Therefore any increased complexity will benefit of write time type checking so that’s what we will do next. For that, we will use… TypeScript. That may sound funny as we are not compiling our front end code but run it as it is. Whether one likes TypeScript or not, it has an awesome feature – type checking without compiling. In fact, it is so clever that it can automatically fetch type script typings of client side libraries you use it they are definitely typed. Also, it is very effective at recognizing types of variables you use in your JavaScript. Right click AspNetCorePreactHtm project in solution explorer and select Manage NuGet Packages. Choose Browse and type typescript in search text box. From matches, select Microsoft.TypeScript.MSBuild package and install the latest version. Right click AspNetCorePreactHtm project in solution explorer and select Add New Item and select npm Configuration File. Keep default name package.json. Replace package.json file content with the following snippet:

JavaScript
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "typescript-lit-html-plugin": "0.9.0"   
  }
}

To use npm packages, you should have Node.js installed. Right click package.json and select Restore packages. This installs typescript-lit-html-plugin. It does not effect operation of solution in any way but highlights html inside template literals. So instead of just red string content color and also should bring some sort of intellisense writing html inside template literal string. By should I mean that it has not worked perfectly for me with Visual Studio 2022 but had no problem with previous versions of Visual Studio. At the time of writing this tutorial, I just don't know any better library for the job. Right click AspNetCorePreactHtm project in solution explorer and select Add New Item and select TypeScript JSON Configuration File. Keep default name tsconfig.json. Replace tsconfig.json file content with the following snippet:

JavaScript
{
  "compilerOptions": {
    // Allow checking JavaScript files
    "allowJs": true,
    // I mean, check JavaScript files for real
    "checkJs": true,
    // Do not generate any output files
    "noEmit": true,
    // Target ES syntax version
    "target": "ES2022",
    // html template literals highlight
    "plugins": [
      {
        "name": "typescript-lit-html-plugin"
      }
    ]
  },
  "exclude": [ "wwwroot/lib" ],
   "include": [ "wwwroot/src" ] 
}

This configuration tells TypeScript to check JavaScript files also while default it only checks TypeScript files (that we don’t have). Also it is not allowed to emit any transpiled code, only check. Latest ECMAScript specification is used for checking. Plugin we installed just before is declared in plugins. In your try to run project now, you will notice that it won’t compile anymore but gives number of error messages concerning Preact + HTM standalone package because TypeScript doesn’t find typings for the file. This is solved by opening installed package wwwroot/lib/htm/preact.standalone.module.js in editor. Add new line before JavaScript and type or paste:

JavaScript
//@ts-nocheck 

This will notify TypeScript not to check its contents. Now the project should compile and run as before.

JSDoc Strongly Typed Type Checking

Now client end has some kind of type checking but it is not very strong as TypeScript can only try to guess types of our JavaScript variables. How can we define their typing without TypeScript code? Answer is JSDoc definitions that are defined as JavaScript comment annotations and do not modify JavaScript itself in any way. TypeScript understands these annotations and can use them for type checking. Let’s use App.js as simple example. At App.js file, place cursor at start of line Navigate(toPage) { and type “/**”. Editor will create JSDoc template like this for you automatically:

JavaScript
/**
    * 
    * @param {any} toPage
    */

Replace it with the following snippet:

JavaScript
/**
     * Navigates to page by name
     * @param {string} toPage Page name to navigate
     */

Where Navigates to page by name is generic definition of the function and @param {string} toPage Page name to navigate defines function parameter toPage to be string type with annotation comment. When writing JavaScript code intellisense will now serve these to you as you write. Try to hover mouse over this.Navigate calls at template literal part of component and you will right away see how intellisense provides just annotated information about the function. Try to change this.Navigate(null); call in components constructor section to this.Navigate(1);. You will get warning as number 1 is not assignable to string. Basically any class, structure or variable can be typed by JSDoc and you are free to type only the ones you see suitable.

Shared Models

There are number of libraries to convert C# models to TypeScript. I assumed that there would be ones to convert C# models to JSDoc annotated ECMAScript classes also and I was surprised when I did not find any. Getting to know Roslyn code analysis had been a long time in my to-do list so I made a small library with it to convert C# models to JSDoc annotated ECMAScript classes. It is not very streamlined but if your models are not too complex, it should get the job done. You can download it from GitHub. Compile program to generate CSharpToES.exe. Once compiled, it can be used at the project (or any other project). Right click project root at Solution Explorer and choose Properties. Navigate to Build and Events. To pre-build event, add the following text:

<path to CSharpToES.exe> $(ProjectDir)Shared $(ProjectDir)wwwroot\src\shared 

Where <path csharptoes.exe="" to=""> should be something like:

C:\CSharpToES\CSharpToES\bin\Release\net6.0\CSharpToES.exe 

depending on where you downloaded it on your computer. Of course, you can also copy compiled content to easier path like C:\CSharpToES\CSharpToES.exe and reference it there. $(ProjectDir)Shared then tells CSharpToES relative source folder of shared C# models and $(ProjectDir)wwwroot\src\shared relative destination folder for compiled js class files as it reads C# models from files, not by reflection. Conversion must happen pre-build, otherwise compiler cannot catch compile errors for JavaScript code that uses compiled code. CSharpToES follows source folder structure so that each .cs file is compiled to equivalent .js file. To demonstrate this is actually the reason why WeatherForecastSummary.cs and WeatherForecast.cs file are put in separate Shared folder and divided in two separated files. If you compile or run project, ECMAScript conversion will be done and equivalent WeatherForecastSummary.js and WeatherForecast.js files are created in wwwroot/src/shared folder. WeatherForecastSummary.js will look like this:

JavaScript
/**
* Forecast feel enum definition
* @readonly
* @enum {number}
* @property {number} Freezing Feels freezing
* @property {number} Bracing Feels bracing
* @property {number} Chilly Feels chilly
* @property {number} Cool Feels cool
* @property {number} Mild Feels mild
* @property {number} Warm Feels warm
* @property {number} Balmy Feels balmy
* @property {number} Hot Feels hot
* @property {number} Sweltering Feels sweltering
* @property {number} Scorching Feels scorching
*/
export const WeatherForecastSummary = {
    /** Feels freezing */
    Freezing: 0,
    /** Feels bracing */
    Bracing: 1,
    /** Feels chilly */
    Chilly: 2,
    /** Feels cool */
    Cool: 3,
    /** Feels mild */
    Mild: 4,
    /** Feels warm */
    Warm: 5,
    /** Feels balmy */
    Balmy: 6,
    /** Feels hot */
    Hot: 7,
    /** Feels sweltering */
    Sweltering: 8,
    /** Feels scorching */
    Scorching: 9
}

JavaScript doesn’t have enum type but this approach is pretty close it. Any comments in C# model are compiled to JSDoc annotations to help front end coding. WeatherForecast.js will look like this:

JavaScript
import { WeatherForecastSummary } from './WeatherForecastSummary.js';

/** Weather forecast class definition */
export class WeatherForecast {

    // private values
    /** @type {Date} */ #Date;
    /** @type {number} */ #TemperatureC;
    /** @type {number} */ #TemperatureF;
    /** @type {WeatherForecastSummary} */ #Summary;

    /** Weather forecast class definition */
    constructor() {
        this.#Date = new Date();
        this.#TemperatureC = 0;
        this.#TemperatureF = 32;
        this.#Summary = WeatherForecastSummary.Cool;
    }

    /**
    * Forecast date time
    * Server type 'DateTime'
    * @type {Date}
    */
    get Date() {
        return this.#Date;
    }
    set Date(val) {
        if (val instanceof Date) {
            this.#Date = val;
        }
    }

    /**
    * Server type 'int' custom range -50 ...  100
    * @type {number}
    */
    get TemperatureC() {
        return this.#TemperatureC;
    }
    set TemperatureC(val) {
        if (typeof val === 'number') {
            this.#TemperatureC = (val < -50 ? -50 : 
            (val >  100 ?  100 : Math.round(val)))
        }
    }

    /**
    * Server type 'int' custom range -58 ...  212
    * @type {number}
    */
    get TemperatureF() {
        return this.#TemperatureF;
    }
    set TemperatureF(val) {
        if (typeof val === 'number') {
            this.#TemperatureF = (val < -58 ? -58 : 
                                 (val >  212 ?  212 : Math.round(val)))
        }
    }

    /**
    * Forecast summary enum value
    * Server type enum 'WeatherForecastSummary' values [0,1,2,3,4,5,6,7,8,9]
    * @type {WeatherForecastSummary}
    */
    get Summary() {
        return this.#Summary;
    }
    set Summary(val) {
        if ([0,1,2,3,4,5,6,7,8,9].includes(val)) {
            this.#Summary = val;
        }
    }

    /** WeatherForecast JSON serializer. Called automatically by JSON.stringify(). */
    toJSON() {
        return {
            'Date': this.#Date,
            'TemperatureC': this.#TemperatureC,
            'TemperatureF': this.#TemperatureF,
            'Summary': this.#Summary
        }
    }

    /**
    * Deserializes json to instance of WeatherForecast.
    * @param {string} json json serialized WeatherForecast instance
    * @returns {WeatherForecast} deserialized WeatherForecast class instance
    */
    static fromJSON(json) {
        let o = JSON.parse(json);
        return WeatherForecast.fromObject(o);
    }

    /**
    * Maps object to instance of WeatherForecast.
    * @param {object} o object to map instance of WeatherForecast from
    * @returns {WeatherForecast} mapped WeatherForecast class instance
    */
    static fromObject(o) {
        if (o != null) {
            let val = new WeatherForecast();
            if (o.hasOwnProperty('Date')) { val.Date = new Date(o.Date); }
            if (o.hasOwnProperty('TemperatureC')) { val.TemperatureC = o.TemperatureC; }
            if (o.hasOwnProperty('TemperatureF')) { val.TemperatureF = o.TemperatureF; }
            if (o.hasOwnProperty('Summary')) { val.Summary = o.Summary; }
            return val;
        }
        return null;
    }

    /**
    * Deserializes json to array of WeatherForecast.
    * @param {string} json json serialized WeatherForecast array
    * @returns {WeatherForecast[]} deserialized WeatherForecast array
    */
    static fromJSONArray(json) {
        let arr = JSON.parse(json);
        return WeatherForecast.fromObjectArray(arr);
    }

    /**
    * Maps array of objects to array of WeatherForecast.
    * @param {object[]} arr object array to map WeatherForecast array from
    * @returns {WeatherForecast[]} mapped WeatherForecast array
    */
    static fromObjectArray(arr) {
        if (arr != null) {
            let /** @type {WeatherForecast[]} */ val = [];
            arr.forEach(function (f) { val.push(WeatherForecast.fromObject(f)); });
            return val;
        }
        return null;
    }
}

This takes an opionated approach to models. Simplest approach would have been to declare properties in constructor. That way doesn’t protect properties as properties would simply become typed fields and bug in JavaScript could then write any value to them. I wanted protect properties so that they could not be for example nulled at client side if C# source property is not marked nullable. This is done by private # fields, which contains actual property value. Private field is then only accessed by getters and setters just like in C#. As C# has number of different numeric types and JavaScript basically only has one setters have code to limit value to limits of C# data type. Downside of this approach if increased complexity and increased number of JavaScript code. But as they are made automatically, it is not a big thing. Also, serialization and deserialization of ECMAScript classes need extra work while plain JavaScript objects can be serialized simply by JSON.Stringify and deserialized with JSON.parse. JSON.parse checks if object has toJSON function defined and uses it so serializing these models work normally with JSON.Stringify. Deserialization on the other hand is more tricky. For that, custom static functions are provided: deserialization from JSON string, from plain JavaScript object or array of them. Library supports the following features:

  • Automatic import export generation between files
  • Dates are handled as date object instead of string
  • C# Dictionaries are converted to JS Maps instead of plain objects because Map can be strongly types with JSDoc
  • Simple initialization of properties and fields is supported so that creating new object at JavaScript would have identical values than new object in C#
  • Simple inheritance

If you hover f.Date, f.TemperatureC, f.TemperatureF or f.Summary variables at FetchData.js, you’ll notice that intellisense doesn’t have a clue what they are. Any misspelling could crash the application at run time. To demonstrate benefits of shared model, we’ll modify FetchData component. Replace FetchData.js with the following code:

JavaScript
"use strict";

import { html, Component } from '../lib/htm/preact/standalone.module.js';
import { WeatherForecast } from './Shared/WeatherForecast.js';

var feelsLike = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", 
                 "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

/**
 * @typedef {Object} FetchDataState FetchData component state structure
 * @property {WeatherForecast[]} forecasts array of WeatherForecast class instances
 * @property {boolean} loading true = values still loading from server, 
 * false = values has been loaded from server
 */

export class FetchData extends Component {

    constructor(props) {
        super(props);
        /** @type{FetchDataState} */ this.state = { forecasts: [], loading: true };
    }

    componentDidMount() {
        this.populateWeatherData();
    }

    async populateWeatherData() {
        const response = await fetch('api/weatherforecast');
        const json = await response.json();         
        this.state.forecasts = WeatherForecast.fromJSONArray(json);        
        this.state.loading = false;
        this.forceUpdate();
    }

    render() {
        if (this.state.loading) {
            return html`<p><em>Loading...</em></p>`;
        }
        else {
            return html`
<div>
    <h1>Weather forecast</h1>
    <p>This component demonstrates fetching data from the server.</p>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
    ${this.state.forecasts.map(f => html`
            <tr>
                <th scope="row">${f.Date.toLocaleString()}</th>
                <td>${f.TemperatureC}</td>            
                <td>${f.TemperatureF}</td>
                <td>${feelsLike[f.Summary]}</td>
            </tr>
    `)}
        </tbody>
    </table>    
</div>
`;
        }
    }
}

Now page has types defined for WeatherForecast and FetchData components internal state. WeatherForecast typing are imported from shared model. If you hover over f.Date, f.TemperatureC, f.TemperatureF or f.Summary variables, intellisense shows its data type and comments written in C# model. If you misspell, for example, f.Summary to f.Summary, the compiler detects that WeatherForecast class does not have property Summary and project will not compile. Also generated array deserializer is used in WeatherForecast.fromJSONArray(json) call. If you want to define type of component’s internal state object type definition is probably easiest like here is done with FetchDataState object definition. One thing to note is that using SetState mutation to change state and trigger render is not recommended as intellisense does not understand that setState mutation refers to internal state object. That is why here in populateWeatherData function, state is altered directly and then by forceUpdate call, Preact is notified that state has changed and render is needed.

Bundling and Minification

Now template is fully functional and supports type definitions. It can be published as it is and modern browsers, that support modules, can run it. There still are a few problems:

  1. JavaScript is not bundled. Browser has to fetch all files one by one by parsing import calls causing performance hit especially if there is lot of source files
  2. Files are send to browser unminified
  3. Your JavaScript source code is fully exposed

Lets start with bundling. Open package.json and replace content with the following:

JavaScript
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "scripts": {
    "build-babel": "babel wwwroot/src -d wwwroot/babel",
    "build-rollup": "rollup wwwroot/src/App.js --file wwwroot/bundle.js --format esm",
    "build-rollup-babel": "rollup wwwroot/babel/App.js 
     --file wwwroot/bundle.js --format esm",
    "build-terser": "terser --compress --mangle -- wwwroot/bundle.js > 
     wwwroot/bundle.min.js",
    "trash-babel": "trash wwwroot/babel",
    "build-purgecss": "purgecss 
     --css wwwroot/lib/bootstrap/css/bootstrap.min.css --content wwwroot/bundle.js 
     --output wwwroot/bundle.min.css",
    "build-all": "npm run build-rollup && 
     npm run build-terser && npm run build-purgecss",
    "build-all-babel": "npm run build-babel && 
     npm run build-rollup-babel && npm run build-terser && 
     npm run build-purgecss && npm run trash-babel"
  },
  "devDependencies": {
    "@babel/cli": "7.17.6",
    "@babel/core": "7.17.5",
    "purgecss": "4.1.3",
    "rollup": "2.69.0",
    "terser": "5.12.0",
    "trash-cli": "5.0.0",
    "typescript-lit-html-plugin": "^0.9.0"
  },
  "dependencies": {
    "babel-plugin-htm": "3.0.0"
  }
}

Then right click package.json and select Restore packages. Packages appear to new folder node_modules at root of project. There are number of packages added and here’s shortly what they do:

  • Rollup bundles multiple ECMAScript files into one
  • Terser minifies bundled ECMAScript files
  • Babel converts with babel-plugin-htm template literals to Preact nodes as described at HTM project’s site
  • trash-cli is used to clean up temporary files created by Babel
  • PurceCSS minifies CSS by parsing from JavaScript files which CSS styles are actually used in application and removes all the rest Scripts are the actual calls to libraries. Scripts can be also bundled into one call and they are then processed that order. Two such calls are defined: build-all and build-all-babel. Let’s try build-all first. Right click project at solution explorer and select Open In Terminal. In terminal, type or paste:
npm run build-all 

and hit enter. Files bundle.js with bundle.min.js and bundle.min.css appear in wwwroot. Script runs first subscript build-rollup which creates bundle.js file Open bundle.js and you will notice that rollup has bundled all our code in one file and shortened variable names where possible. All comments, tabs and linebreaks are still there. Next script runs subscript build-terser which minifies just created bundle.js. Open bundle.min.js and you will notice that all comments, tabs, line breaks and whitespaces are removed from JavaScript code parts. Template literals of components however are as they were before. This is because they are not code but strings, Terser cannot know what we are using it for. build-purgecss tells PurgeCSS to extract all CSS from bootstrap.min.css that is actually used in created bundle bundle.js and write result to bundle.min.css file. Source code is now bundled and minified but one step further can be made. Babel with babel-plugin-htm can compile component template literals to Preact nodes. If makes bundle slightly smaller thus it loads faster and browser JavaScript parser does not have to compile them as they are already compiled which accelerates code startup at browser. Before running babel, a config file needs to be defined. Right click project, Add / New Item / JSON file, name it babel.config.json and paste the following to file:

JavaScript
{
  "presets": [],
  "plugins": [
    [
      "babel-plugin-htm",
      {
        "tag": "html",
        "import": "../lib/htm/preact/standalone.module.js"
      }
    ]
  ]
}

This tells babel to use babel-plugin-htm and where Preact standalone module is located. In terminal, type or paste:

npm run build-all-babel 

and hit enter. This adds one step at the previous chain: before giving code for Rollup to bundle it runs build-babel step which triggers babel to compile each of our JavaScript file to wwwroot/babel folder. Open wwwroot/babel/App.js and you will notice that Babel along with babel-plugin-htm has compiled template literal in render function to Preact h node calls where h corresponds to React’s createElement. Rest of the script is the same as in build-all except Rollup is directed to use these Babel compiled source files instead of the original ones. If you open bundle.min.js, you will notice that template literals have gone and all application JavaScript is on minified line. Bundling and minification is needed only prior to publish, it is not needed for debugging. You can run it from terminal when publishing but if you have memory like mine, it can be automated for example by adding it to project’s build events by right clicking project and selecting Properties. Then at build events section, add npm run call to Post-build events. In that case, script will run each time you compile project.

Publishing

Now we have bundled and minified JavaScript and CSS for publishing. If we publish project now, they will not be used as html in Pages\Shared_Layout.cshtml still calls our source code at wwwroot/src and everything that is in wwwroot is published. Close project for a second and navigate to project definition file EsSpaTemplate.csproj and open it for example in Notepad (my favourite is Notepad++). After last PropertyGroup definition, paste the following snippet, save and reopen project.

XML
<ItemGroup>
    <Content Update="wwwroot\src\**" CopyToPublishDirectory="Never" />
    <Content Update="wwwroot\bundle.js" CopyToPublishDirectory="Never" />
    <Content Update="wwwroot\babel\**" CopyToPublishDirectory="Never" />
</ItemGroup>

This will prevent publishing of any source code. Maybe this is possible inside Visual Studio also but I am not aware how. Next open Pages\Shared_Layout.cshtml. Replace content with the following:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PreactHtmStarter</title>    

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="~/bundle.min.css" />
    </environment>  

    <script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>        

    <environment include="Development">
        <script defer type="module" src="~/src/App.js" 
                asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script type="module" src="~/bundle.min.js" 
                asp-append-version="true"></script>
    </environment>  

</head>
<body>
    @RenderBody()    
    @RenderSection("Scripts", required: false)
</body>
</html>

Razor engine has a neat feature that it can inject different code to browser in debug mode than in published one. Directives <environment include="Development"> and <environment exclude="Development"> are added for that purpose. Simply, this directs to use the source code in wwwroot/src when debugging and to use minified code when published. Now template is feature ready against what was promised in preface.

I made a little test by publishing default React template to IIS and publishing this template to IIS with following results in Chrome browser.

Default React template needed to load:

192 kb + 8.2 kb ≈ 200 kb JavaScript 165 kb + 573 b ≈ 166 kb css

This template needed to load:

78.4 kb + 16.9 kb ≈ 96 kb JavaScript 10.3 kb css

Where 78.4 kb is bootstrap.bundle.min.js and 16.9 kb is minified code actually written in this tutorial including embedded Preact and HTM.

Summary

Basically, here’s what we get with this template:

  • React components with Preact and HTM template literals in light weight template without need to precompile to JavaScript.

And what we don’t get:

  • React libraries. There are tons of React libraries out there practically for any purpose you might think of. But they all use JSX and hence need compile round

So far, it has not been a problem to me to write necessary components for small web user interfaces I have used this template for. For same reason, I maybe would not use it in large and extensive projects.

Happy coding and thanks if you had stamina to read this far. This tutorial ended up being a lot longer than I expected.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior) TottiWatti
Finland Finland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionProtecting routes based on authenticated users Pin
JLichina18-Jan-24 3:58
JLichina18-Jan-24 3:58 
AnswerRe: Protecting routes based on authenticated users Pin
TottiWatti23-Jan-24 0:56
TottiWatti23-Jan-24 0:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.