An MVC-Knockout-TypeScript-Webpack Starter project

Are you considering using KnockoutJS with TypeScript and Webpack for a single page application in an ASP.NET MVC context? Then this starter project is for you.

Recently I worked on a relatively complex single page app with KnockoutJS and TypeScript. The result of what I’ve learned during this project is incorporated in this MVC-Knockout-TypeScript-Webpack starter project. The aim is to provide a quick start for anyone using the same technology stack.

In the best tradition of JavaScript framework tutorials, the application which we’ll set up through the starter project is a single-page application for the booking of superheroes.

The focus is very much on the use of Knockout components. A structure is provided for building up a single page application out of Knockout components. This includes a structure for setting up form steps, which are also implemented as Knockout components. By way of example, the starter project includes a few form steps, as well some components used in the various form steps, such as a list of superheroes, a shopping bag and a discount selector. The elements in the screenshot indicated by the red arrows are separate Knockout components.

Components on the first form step

I have a back-end programming background myself. After doing some work recently with Angular 2 and TypeScript, I was really enthusiastic about TypeScript, because of the similarities with programming in C#, including strong typing and the object-oriented structure. So TypeScript seemed a logical choice for the project.

KnockoutJS was chosen because of the good compatibility with older browsers.

One of the issues this project seeks to address is the need for bundling and minification. No problem in a run-of-the mill ASP.NET MVC project, but rather different in single-page application using require syntax in the JavaScript. In this project, Webpack is used for bundling and minification.

 

Getting started

Download from GitHub

You can download the project from GitHub at https://github.com/johnligt/Knockout-TypeScript-Webpack-Starter

Visual Studio configuration

Because we will be working with TypeScript make sure you have TypeScript installed in Visual Studio, through Tools > Extensions and updates

Installation

The project needs Node.js, TypeScript, Webpack and Knockout JS.

Install Node if you haven’t got Node on your machine already. You can find Node.js at https://nodejs.org/en/download/

The project includes a package.json file in which all dependencies are configured.  To install these dependencies, open a command window in admin mode,  in the root of the web project, and run: 

npm install

To generate the JavaScript files which the project uses, you need to run Webpack. You do this by opening a command window in the root of the web project, and typing:

webpack --watch

If you build the project in Visual Studio you should now be able to start the application with Ctrl-F5.

Debugging Knockout in Chrome

To enable debugging of Knockout in Chrome, install the Knockoutjs Context Debugger, available through:

https://chrome.google.com/webstore/detail/knockoutjs-context-debugg/oddcpmchholgcjgjdnfjmildmlielhof

This tool enables you to select an element in the page through the Chrome developer tools, and view the properties and values of the related Knockout elements, if any.

knockoutjs_context_debugger

Project structure and configuration

I won’t attempt to provide an exhaustive description of the project, but some highlight and main features are presented.

TypeScript configuration

The project includes a TypeScript configuration file in the root of the web project, “tsconfig.json“. This file overrides the TypeScript build setting in your Visual Studio project properties. Because we’ll be saving the TypeScript source files in a directory called “source”, the key “baseUrl” is added under the compiler options:

"baseUrl": "./source/"

The target key set at “es5” for maximum compatibility with older browsers, i.e. the TypeScript compiler will transpile the TypeScript code to EcmaScript 5 compatible JavaScript.

More information on TypeScript can be found at https://www.typescriptlang.org/docs/tutorial.html

Type definition files

For TypeScript’s strong typing to work, TypeScript needs some information on the types in various libraries. These are supplied by type definition files, with the extension  .d.ts

The files  can be added to your project through Nuget, GitHub (https://github.com/DefinitelyTyped/DefinitelyTyped ) or the node package manager. In the starter project a directory with type definition files for some important libraries is included under the root. Type definition files for Knockout, Require and jQuery are included.

Making Knockout globally available to enable debugging

To enable debugging with the Knockoutjs Context Debugger in Chrome when using Webpack, the “ko” variable needs to be made available as a global object. We do this using the “expose-loader” (https://github.com/webpack/expose-loader) . This loader is referenced in package.json. It is used in Main.ts, the starting point in the project: require("expose?ko!knockout");

After globally exposing the “ko” variable in this way, we don’t need to reference it anymore in the various TypeScript files which use Knockout, i.e. it is not necessary anymore to place

import ko = require("knockout");

at the top of the TypeScript files which require Knockout.

The Visual Studio solution

The project is set up as a Visual Studio solution, consisting of two projects, Web and Data. Web contains the web application as well as the single-page application. Data is a class library project containing some additional models and helper classes for data provision.

The host MVC view for the single page application

The original project from which this starter project is derived is an ASP.NET MVC application with a commercial CMS system. In this project, we’ve kept things simple, we just have one MVC controller and view to host the single page application. In your real-world application this could be any kind of web page.

We have some basic ASP.NET MVC ingredients: a HomeController, a corresponding MVC view (index.cshtml), and a _Layout.cshtml.

Views > Home > Index.cshtml contains the starting point of our single page application.

solution_explorer_1

The structure of the single page application

The basic structure for the single page application is a folder called “source” for the TypeScript files and JavaScript libraries, and an output directory called “build”, both under the root. The application specific files will be placed under the directory “source/app”, the libraries will placed under “source/lib”.

solution_explorer_2

The directory App/Components contains the application-specific components such as the list of superheroes and the discount selector.

The directory App/FormFields contains more generic components, such as components wrapping input fields and label components.

The directory App/Services contains classes which take care of obtaining data from the MVC backend through calls to api controllers.

Webpack

Webpack is listed as a dependency in package.json, so it should be installed after running  npm install

To generate the JavaScript files which the project uses, we need to run Webpack. We do this by opening a command window in the root of the web project, and typing:

webpack --watch

This will generate the file app.bundle.js and vendor.js in the Build folder. If you want to stop the watch process, type Ctrl-C in the command window.

We refer to app.bundle.js in our _Layout.cshtml file:

<script src="~/Build/app.bundle.js"></script>

Configuration of Webpack is done through the webpack.config.js file in the root of the Web project.

One of the things configured is the entry point of the application. In our case this is Main.ts, in the root of the App folder. We do this by referring to this file in webpack.config.js, by setting the entry key to Main.ts

entry: {
    app: "./App/Main.ts"
}

This path is relative to the context, which is set through the context key:

context: path.resolve("./Source/")

The location of the Webpack output is also set in the configuration file:

output: {
        path: path.resolve("./Build"),
        publicPath: "./Build/",
        filename: "[name].bundle.js"
    }

When you run the webpack command, the output is not minified. To generate minified output run:

webpack --env.prod

For more information on Webpack see: https://webpack.github.io/docs/tutorials/getting-started/

KnockoutJS

The starter application is built up out of Knockout components, supported by some additional service classes.  For more information on Knockout components, see: http://knockoutjs.com/documentation/component-overview.html

The starting point is a component called MainForm. This is the only component which is directly referred to in the Razor view “~/Views/Home/Index.cshtml”. This component hosts all other components, directly or indirectly, as components may also host other components. MainForm is mainly a container for the other components, in which the actual work will be done.

Every component consists of a viewmodel and an html template. In this project, the viewmodel and template of every component are kept together in a single directory. To facilitate the creation of a new components, the folder \Source\App\Components\Template\ contains a component template. You can copy this folder, replace the word “template” by your component name, in the file and folder names as well as in the names of the classes, and register the component.

To be able to use a Knockout  component, the component has to be registered first. Because the number of components in a large single page application may be substantial, a separate file is used for component registration, ComponentRegistration.ts. The form steps are also implemented as components, so they are also registered in this file.

For instance, the MainFormComponent is registered like this:

ko.components.register("indi-main-form", new MainFormComponent());

In the Razor view “~/Views/Home/Index.cshtml” we refer to this component like so:

<indi-main-form></indi-main-form>

In the TypeScript object “MainFormComponent” the component is instantiated with the viewmodel and the template of the component:

export class MainFormViewModel {

    constructor(params) {
        
    }

}

export class MainFormComponent {

    constructor() {

        return {
            viewModel: MainFormViewModel,
            template: require("text!./MainFormView.html")
        }
    }
}

Nothing is happening in the viewmodel class MainFormViewModel in this particular instance, but generally this is where all the work is done. This structure, in which a separate component class to set up the component in the constructor, is used for all Knockout components in the project. This is done so the html template and the viewmodel can be kept together in a single folder, with relative paths.

Initializing the single-page application

The entry point for the single-page application is Main.ts. The class Main takes care of the following:

  • load the file in which the Knockout components are registered
  • load the data needed for the application (heroes, prices) by calling the appropriate methods in the service classes.
  • bind the main Knockout viewmodel.
  • initialize Knockout validation.

The main viewmodel in this application is used to hold the booking details which result from the various choices the customer makes during the booking process. This viewmodel, called BookingData, lives in a separate TypeScript file, called BookingData.ts

The service classes

To gain access to the data which the application needs (labels texts, products, prices) the single-page application includes some service classes, which generally make a call to a web api controller supplying the data, and store the data in a static variable on the class. These classes may also contain some business logic which must be globally available.

Because the data must be loaded before the components can be shown on the page, and because there may be dependencies between the various services, promises are used when calling the methods in the service classes which are responsible for loading the data. These methods are called from Main.ts,  in the appropriate order.

For instance PriceService.ts takes care of getting the prices from the backend, stores the prices in a static variable priceList,  and contains a helper function to select other prices when a different discount level is chosen.

require(["jquery"]);
import es6promise = require("es6-promise");
import { Price } from "App/Models/Price";
import { ProductService } from "App/Services/ProductService";

export class PriceService {

    private static priceServiceUrl = "/api/prices/";

    static priceList: Price[];

    static selectedDiscount: KnockoutObservable<DiscountEnum>;
    
    static getPriceList(): any {

        let promise = new es6promise.Promise((resolve, reject) => {

            let request = $.ajax({
                dataType: "json",
                url: PriceService.priceServiceUrl    
            });

            request.done((data) => {

                PriceService.priceList = <Price[]>data;

                PriceService.selectedDiscount = ko.observable(DiscountEnum.DefaultDiscount);
                
                console.log("Price list initialized");
                resolve(request.responseJSON);
                
            });
            
            request.fail(
                (jqXhr, textStatus) => {
                    const message = "Request in PriceService failed: " + textStatus;
                    console.log(message);
                    reject(message);
                }
            );

        });

        console.log("Returning Price promise");

        return promise;
    }


    static setPrices(): void {
        
        for (let product of ProductService.productList) {
            
            const productPriceObject = PriceService.priceList.filter(x => x.productId === product.productId)[0];

            if (productPriceObject === undefined || productPriceObject === null) {
                continue;
            }
            
            switch (PriceService.selectedDiscount()) {
                case DiscountEnum.NormalDiscount:
                    product.productPrice(productPriceObject.productDiscountPrice);
                    break;
                case DiscountEnum.SuperDiscount:
                    product.productPrice(productPriceObject.productSuperDiscountPrice);
                    break;
                default:
                    product.productPrice(productPriceObject.productDefaultPrice);            
            }
            
        } 
    }
}

export enum DiscountEnum {

    DefaultDiscount,
    NormalDiscount,
    SuperDiscount
}

 

Form steps

The starter application provides basic plumbing for multiple form steps and navigation through those form steps.

The app consists of four example form steps: product selection, entering personal details, a “check and submit” step, and a “thank you” step. The form steps are also implemented as Knockout components, inheriting from a FormStepBase component.  Remove them and add your own form steps as needed.

Adding a form step requires the following steps:

  1. Add a subfolder to the /Source/App/FormSteps/ folder with the name of your form step.
  2. Add the viewmodel and template files.
  3. Register the form step component in ComponentRegistration.ts
  4. Add your form step to FormStepEnum
  5. Add your form step to  MainFormView.html, as a nested component, and assign an order value to the form step.

MainFormView.html in the starter project looks like this:

<div class="row">
 <div class="col-md-8">

   <indi-formstep-selectproducts params="order: 0"></indi-formstep-selectproducts>
   <indi-formstep-personaldetails params="order: 1"></indi-formstep-personaldetails>
   <indi-formstep-check params="order: 2"></indi-formstep-check>
   <indi-formstep-thanks params="order: 3"></indi-formstep-thanks>

   <indi-navigation></indi-navigation>

 </div>
 <div class="col-md-4">
   <indi-shopping-bag></indi-shopping-bag>
 </div>
</div>

Every form step viewmodel has two properties with which the visibility of the form step can be manipulated, active and visible. These properties are inherited from FormStepBase.

   this.visible = ko.observable(false);
   this.active = ko.observable(true);

To disable a form step due to some business rule, you can set active to false, and the form step will never be shown.

To toggle the visibility as the user steps through the form, the “visible” property is set to true or false. This is taken care of in Navigation.ts and FormStepsManager.ts

Validation

For validation, the Knockout-Validation library is used. More information on this library can be found at https://github.com/Knockout-Contrib/Knockout-Validation or https://www.pluralsight.com/courses/knockout-validation-library

Because validation usually happens when moving to the next form step, a validation model needs to be set up for each form step. This model is instantiated in FormStepBase. Every nested component which has to be validated needs a reference to this validation model, so form field components such as the text field and textarea components (in the folder \Source\App\FormFields\) inherit from a base class FieldBase, which sets up the reference to the validation model of the current form step.

Conclusion

Hopefully this starter application will give you a head start with your application. If you have any questions or suggestions, please let me know.

Thanks to Guido de Ram who did much of the work on Webpack and Knockout Validation in the original project from which this starter application is derived.

 

  • Jason

    tsconfig.json is missing in the project, so I am new to ts and this is a major stumbling block for me, any help on this one?

    • John Ligtenberg

      Hi Jason, I’ve just added tsconfig.json to the repo.

  • Jason

    thanks for the upload of the tsconfig.json file. I am using your project as a basis for rewriting an app I have been building. I have hit some issues, mainly due to coming over to typescript for the first time.
    One issue I have is that I have some javascript inline in my component html that uses numeral.js. I get an error that numeral is not defined, however I can use numeral in the js code of the very same component, and I cannot figure out why this is happening?
    I tried adding numeral as new webpack.ProvidePlugin in the webpack config but this still comes up, any suggestions?