Skip to main content

Custom SPA engine, similar to MVC pattern by using TypeScript

Single Page Application, a fundamental requirement for SPA is to develop the application with XMLHttpRequest (XHR) or AJAX. That is only main requirement, other things come as add-on stuff.

Advancements of SPA?

In today's world, we do not want to be restricted with basic features or functionalities. We want everything like any new Phone comes in market with some new shiny feature. There are a lot of functionalities available with SPA libraries and could be extended with more extension to have richer and easier implementation. To be specific we shall refer it as frameworks, not libraries.

Earlier days one-way, two-way data binding with templates were very minimum things to expect from frameworks. Now they talk about component structure, Virtual Dom, IOC, state management, performance etc.

Why building own framework/library?

What I believe and experienced is each SPA JS Frameworks has its own set of rules and structure that has to be followed. If we want to have our own dynamic things or we want to change some structure than we have to dig down into that framework, understand and change accordingly. I do not mean it is undoable or not flexible to change but we got to spend more time with it and when we have to switch to another SPA framework, so it's of no use. I understand this is a general thing in the Developer world but these frameworks have a completely different approach with each other. Thankfully with advancements of ECMAScript things are lining up.

If we have to follow structure than why not our own with the features we need and structure that we like.

I guess that is it, if you do not want to look into another implementation, you should stop reading. The implementation is not any way similar or having greater features than existing SPAs frameworks. Maybe you would find it limited, restricted, incomplete etc. But I can tell you that it can work on any medium to large enterprises application with Agile development, manageable client-side codes. 

Why?

My vision/requirements for creating this are as follows:
  • Code Manageability and easy refactoring.  
  • MVC like structure, similar to server-side codes.
  • Design as components, each component would be applicable to single view/feature, small area.
  • Easy to change. (Maybe I am repeating again)
  • Easier to develop, with minimum or small learning curve.
  • My favorite OOPS, with proper way/channel to communicate.
  • Using jQuery. I know for many of us, the story ends here. SPA frameworks provide abstractions and recommend not to use it for performance reason but a lot of times even after provided abstraction we end up using it. For me, it just easier way and the most strong reason is there are a lot of plugins available which are easy-peasy with different version and personalized flavors based on requirement. [Probably this is going to change in future, with all those advancements and avoiding of DOM]
There could be few/much more but those are primary items.

What are tech/tools/libraries/technique chosen? Why those are chosen? 

  • TypeScript: This is just used for writing OOPS codes and other features from TS in an easier way. Direct JS would take a lot of codes.
  • jQuery: Interacting with DOM and with various plugins.
    • jQuery Validation and jQuery Unobtrusive Validation: The validation implementation for the forms. As of now, we would be looking into implementation to add validation unobtrusively by directly putting it on HTML data attributes.
  • Handlebars.js: The template engine to merge our Models with HTML. The HTML/View rendering would be taken care by this library.
  • InversifyJS: The name itself says something related to IOC. Through this, we would be using DI, as need basis. In this architecture, we are going to avoid creation of instance manually. This would allow us to fuse classes with other dependent class in much easier manner and loosely coupled approach.
  •  DirectorJS: This is our routing library, which would allow us to have client-side routing. There are various client-side routing available, but I liked the approach with the forward routing which would automatically resolve any route if a user requests any specific deep URL path. Also, it could be separated into various places of codes for better code manageability.
  • Any client side UI framework: Bootstap, MaterialUI etc.
  • SCSS[Optional]: This just a better way to manage our CSS.
  • WebPack: This is a really important piece of tool to compile all of our client-side codes. The responsibilities of it are TS Compilation, Handlebar compilation, used library compilation, HMR[Optional], SCSS compilation. 
  • Guidelines: The other very point to is to follow guidelines, like naming files, folders, not using DOM elements in codes, DI implementation and it's usage, routing approach. I would discuss these as we progress with the article.
Before jumping into codes let's talk what are we trying to achieve here and what all are processes to achieve same

  • Single Page application.
  • The whole flow of the application needs to be derived through routing. Director would be used for same.
  • Component or self-contained package structure. When we say Login, Password recovery, these would be self-packaged items. If for some reason those needs to communicate with each other than it has to be through callback approach, similar to events, constructor injection.
  • Any type of data posting has to be wrapped with form and that would be taken care by helper functions to automatically post and receive data and populate warnings in case of any error. 
  • There could not be any direct dependency between two components, meaning we should not create an object manually (except few cases), it has to be received through wrapper function which would be using Inversify DI. 

Implementation: starting with configurations and code structures

Enough of talking, let's get our hands dirty with some coding and implementation now. This is going to have a lot of information. I would try to explain all major parts of it. I have already put codes in Github, can have a look and then go through details here.
https://github.com/viku85/CustomSpaApp

This is not going to have full details for each and everything, it may need some prior knowledge but I would try to give details to easily start with.

The first thing webpack configuration which will build the entire application.

webpack.config.js

 var path = require("path");  
 var webpack = require("webpack");  
 var ExtractTextPlugin = require("extract-text-webpack-plugin");  
 module.exports = {  
   //watch: true,  
   cache: true,  
   entry: {  
     Vendor: "./clientapp/infrastructure/vendor.ts",  
     App: './clientapp'  
   },  
   output: {  
     path: path.join(__dirname, "/wwwroot/app"),  
     filename: "SpaApp.[name].js",  
     chunkFilename: 'chunks/[name].chunk.js',  
     libraryTarget: 'umd',  
     library: ['SpaApp', "[name]"],  
     publicPath: '/app/'  
   },  
   resolve: {  
     extensions: ['.ts', '.js', '.webpack.js', '.web.js'],  
     alias: {  
       'handlebars': 'handlebars/runtime.js'  
     }  
   },  
   devtool: 'source-map',  
   module: {  
     loaders: [{  
       test: /\.ts$/,  
       loader: ['ts-loader'],  
       exclude: /node_modules/  
     },  
     {  
       test: /\.scss$/,  
       use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({  
         fallback: "style-loader",  
         use: [{  
           loader: 'css-loader'  
         }, {  
           loader: 'sass-loader',  
           options: {  
             includePaths: [  
               path.resolve(__dirname, 'node_modules'),  
               path.resolve(__dirname, 'node_modules/@material/*')]  
           }  
         }  
         ]  
       }))  
     },  
     {  
       test: /\.css$/,  
       use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({  
         fallback: "style-loader",  
         use: 'css-loader'  
       }))  
     },  
     {  
       test: /\.(png|jpg|jpeg|gif)$/,  
       loader: 'file-loader',  
       exclude: /node_modules/  
     },  
     {  
       test: /\.(eot|svg|ttf|woff|woff2)$/,  
       loader: 'file-loader?name=fonts/[name].[ext]'  
     },  
     {  
       test: require.resolve("jquery"),  
       loader: "expose-loader?$!expose-loader?jQuery"  
     },  
     {  
       test: require.resolve("material-components-web"),  
       loader: "expose-loader?mdc"  
     },  
     {  
       test: /\.handlebars$/,  
       loader: "handlebars-loader"  
     }  
     ]  
   },  
   plugins: [  
     new ExtractTextPlugin('style.css'),  
     new webpack.LoaderOptionsPlugin({  
       debug: true,  
       minimize: true  
     })  
   ]  
 };  

To compile things, webpack uses different loaders available through NPM. We can filter out files with test attribute and put loader library name in it. You can see, for various files, there are different loaders which can also be chained up to have input from an output of another dependent loader.

The other take away from config.
   entry: {   
    Vendor: "./clientapp/infrastructure/vendor.ts",   
    App: './clientapp'   
   },   

This would generate two files, one Vendor which will include files from different libraries as one, the other App file would be a combined file from different TS code files.

The provided configuration value are the path of code files. The reason vendor file is directly mapped is we want to expose that file directly whereas ./clientapp would be using Index.ts. The idea with Index.ts is to keep exporting files which need to be public. So, in that sense, each folder under Clientapp can have Index.ts to keep exporting files and ultimately ./clientapp/index.ts file would be read to generate, as a final single file. You can see from image, almost all folders have Index.ts under AppArea folder. In the component folder, IndexInternal.ts are present the reason is we do not want those to be public, just need to reference within the Controllers/public facing JS/TS. It has the same structure as Index.ts but referencing those manually by specifying the file name IndexInternal.ts.

A sample code available from index.ts. These exports would be similar on IndexInternal.ts as well.

 export * from './AppArea';  
 export * from './Infrastructure';  

webpack does take input from tsconfig as well for the compilation of TS files.

tsconfig.json

 {  
  "compilerOptions": {  
   "target": "es5",  
   "lib": [ "es6", "dom" ],  
   "module": "commonjs",  
   "sourceMap": true,  
   "sourceRoot": "App",  
   "outDir": "wwwroot/app",  
   "experimentalDecorators": true,  
   "emitDecoratorMetadata": true,  
   "baseUrl": ".",  
   "paths": {  
    "*": [  
     "./ClientApp/CustomTyping/*"  
    ]  
   }  
  },  
  "compileOnSave": false,  
  "exclude": [  
   "node_modules",  
   "wwwroot"  
  ]  
 }  

Offcourse the tsconfig and webpack.config can be modified as per need basis and all those configurations may not be relevant to you or need any additional stuff. Like there could be a different configuration for Dev, Staging, Production and so on.

vendor.ts

As discussed, this file contains all the references of third party libraries which would be resolved through webpack by using NPM packages.

CSS import is done at this place only.
 import 'director/build/director.js';  
 //import 'material-components-web';  
 import 'jquery';  
 import 'jquery-validation';  
 import 'jquery-validation-unobtrusive';  
 import 'bootstrap-sass/assets/javascripts/bootstrap';  
 import 'lodash';  
 import 'reflect-metadata';  
 import 'inversify';  
 import 'handlebars';  
 // CSS  
 import '../Css/Site.scss';  

Using reference of above files in our main HTML

The compiled codes would be generated in wwwroot/app folder. You can see just compiled code JS and CSS are used in _Layout.cshtml. All generated, files would be prefixed with SpaApp as per mentioned setting in webpack.config. There is some incompatibility with DirectorJS, that is why it is directly mentioned.

_Layout.cshtml: This is based on dotnet MVC but can be simple HTML as well.

 <!DOCTYPE html>  
 <html>  
 <head>  
   <meta charset="utf-8" />  
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />  
   <title>@ViewData["Title"] - SpaApp</title>  
   <link rel="stylesheet" href="~/app/style.css" />  
 </head>  
 <body>  
   <nav class="navbar navbar-inverse navbar-fixed-top">  
     <div class="container">  
       <div class="navbar-header">  
         <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">  
           <span class="sr-only">Toggle navigation</span>  
           <span class="icon-bar"></span>  
           <span class="icon-bar"></span>  
           <span class="icon-bar"></span>  
         </button>  
         <a href="#" class="navbar-brand">SpaApp</a>  
       </div>  
       <div class="navbar-collapse collapse">  
         <ul class="nav navbar-nav">  
           <li><a href="#">Home</a></li>  
           <li><a href="#/about">About</a></li>  
           <li><a href="#/about/contact">Contact</a></li>  
         </ul>  
       </div>  
     </div>  
   </nav>  
   <div class="container body-content">  
     @RenderBody()  
     <div class="app-main">  
     </div>  
     <hr />  
     <footer>  
       <p>&copy; 2017 - SpaApp</p>  
     </footer>  
   </div>  
   <script src="https://rawgit.com/flatiron/director/master/build/director.min.js"></script>  
   <script src="~/app/SpaApp.vendor.js"></script>  
   <script src="~/app/SpaApp.app.js"></script>  
   @RenderSection("Scripts", required: false)  
 </body>  
 </html>  

Also to start application need, to include this:

   <script type="text/javascript">  
     var app = SpaApp.App;  
     app.SetConstant(app.HomeViewHolderOption,  
       new app.HomeViewHolderOption({  
         ContainerSelector: '.app-main'  
       }));  
     new app.MasterRoute();  
   </script>  

The route is initialized through new app.MasterRoute() and through this only whole thing would start to work since application would depend on routing. The SetConstant is for DI initialization, would talk about it later.

These were basic implementation/infrastructure to start coding. Before starting let's go through some guidelines that have to be used throughout the application development.

Guidelines to create files, folders, structure

When we talk about JS, there are no constraints which makes it really powerful and easy to make mistakes, that is where TS comes handy with some certain ground rules that can be applied. [Yes, there are Static Code Analysis tools available to force it].

In a similar way, we would need to create some rules that need to be followed while creating TS files. Please relate to shared image as well.

  • Class name and file name should be same.
  • All codes should be present on ClientApp folders.
  • SCSS, CSS files should be created under ClientApp/Css.
  • All the building block for the application are present under InfraStructure folder. The Master Route config is available here.
  • All the codes related to page, features, HTML need to be placed under AppArea folder with any required nested folders. This is where it is similar to MVC just that it has to be managed by your own with folder structure.
  • Keep using Area suffix to divide functionality/features for pages. Each of these areas needs to be treated as self-contained, these should not directly consume others DOM. For example, Home should not directly access DOM for Login. Better to use callbacks for those situations.
  • For each main Area, we would need to have specific route configuration for that area along with any nested area.
  • Component is a folder where all of our helper codes exist, like for any AJAX calls, Form posting, Validations, Pop-Up, Event helpers and so on. It can be again as nested as you want.
These were basic rules, it could have been discussed at last but good to do at first to have better clarity.

Client-side routing

This is the first thing to start with since this would be an entry point of codes/pages that need to be accessed.

These can be sub-divided into multiple, for an example Account routing which can contain login, password recovery etc. would be in a different section than other modules. So, a master route would be using direct child routes, and those child routes can have its own sub child.

Master route

 import { GetController } from './IocConfig';  
 import { AboutAreaRoute } from './../AppArea/AboutArea';  
 import { HomeController, AboutController } from './../AppArea';  
 declare var Router: any;  
 declare var director: any;  
 
 export class MasterRoute {  
   constructor() {  
     this.Init();  
   }  
   Init(): void {  
     var router: any = null;  
     router = Router(this.GetRoutes()).configure({  
       on: this.GetListener(),  
       recurse: 'forward',  
       async: true  
     }).init('/');  
   }  
   GetRoutes() {  
     var aboutAreaRoute = new AboutAreaRoute().InitRoute();  
     var routes = {  
       '/': {  
         on: (next) => {  
           document.title = "Home | SpaApp";  
           GetController<HomeController>(HomeController).Init(next);  
         }  
       }  
     };  
     $.extend(routes, aboutAreaRoute);  
     return routes;  
   }  
   public GetListener() {  
     console.log("Listener at: " + window.location);  
   }  
 }  

Router is provided through DirectorJS which is initialized by putting GetRoutes() function. The implementation is simple with JSON composition. We put master route by using '/' and having code on on callback where we execute codes. But in callback, we are just having GetController<HomeController>(HomeController).Init(next), this is how we would keep getting instance but take away from this is the next it is kind of wrapper for callback. For an example, if someone wants to stop propagating URL from / to any nested route they can block that up through this.

The var aboutAreaRoute = new AboutAreaRoute().InitRoute() is a container of child route which  is added into main route through $.extend(routes, aboutAreaRoute).

Nothing fancy about child route here is the code for above:

 import { GetController, GetObject } from './../../Infrastructure/IocConfig';  
 import { AboutController } from './AboutController';  
 import { AboutViewOption } from './AboutViewOption';  
 import { ContactController } from './ContactArea/ContactController';  
 class AboutAreaRoute {  
   InitRoute() {  
     var route = {  
       '/about': {  
         '/contact': {  
           on: (next) => {  
             document.title = "About | Contact | SpaApp";  
             GetController<ContactController>(ContactController).Init(next);  
           },  
         },  
         on: (next) => {  
           document.title = "About | SpaApp";  
           GetObject<AboutController>(AboutController)  
             .Init(GetObject<AboutViewOption>(AboutViewOption), 'about', next);  
         }  
       }  
     };  
     return route;  
   }  
 }  
 export { AboutAreaRoute }  

There is a huge issue with route and rendering of HTML. When someone comes to nested route by putting URL in a browser than whole route path would be executed from root (/) to nested route. To overcome this issue we would be having a check on each parent controller if any of it's associated child component is rendered. The idea is to restrict re-rendering of parent DOM element if child DOM exists and to achieve it we would be setting HTML data attribute on parent element whenever child element is initialized but in a cleaner way. We would see that once we are into controllers.

Dependency Injection (DI)

In this, we are not going to implement DI based on interface and class but at least constructor injection. This would free us from creating an object if it has multiple dependencies.

We would be creating a wrapper class to get or set injection as needed and setting up controller injection within the same file.

 import { Container, interfaces } from 'inversify';  
 import { HttpRequestResponse, JqueryEventHelper, Form } from './../Component/IndexInternal';  
 import { HomeController } from './../AppArea/HomeArea/HomeController';  
 import { AboutController } from './../AppArea/AboutArea/AboutController';  
 import { ContactController } from './../AppArea/AboutArea/ContactArea/ContactController';  
 import { ContactForm } from './../AppArea/AboutArea/ContactArea/ContactForm';  
 import { BaseController } from './../Base/BaseController';  
 let container = new Container();  
 // Helper  
 container.bind<HttpRequestResponse>(HttpRequestResponse).toSelf().inSingletonScope();  
 container.bind<JqueryEventHelper>(JqueryEventHelper).toSelf().inSingletonScope();  
 container.bind<Notification>(Notification).toSelf().inSingletonScope();  
 container.bind<Form>(Form).toSelf().inSingletonScope();  
 // Controllers  
 container.bind<HomeController>(HomeController).toSelf();  
 container.bind<AboutController>(AboutController).toSelf();  
 container.bind<ContactController>(ContactController).toSelf();  
 container.bind<ContactForm>(ContactForm).toSelf();  
 function GetObject<T>(service: any): T {  
   return IsAlreadyInitilized(service) ? <T>container.get(service) : null;  
 }  
 function IsAlreadyInitilized(service): boolean {  
   return container.isBound(service);  
 }  
 function SetConstant(service: any, instanc: any) {  
   if (!container.isBound(service)) {  
     container.bind(service)  
       .toConstantValue(instanc);  
   }  
   else {  
     console.log(`${service.name} is already registered.`);  
   }  
 }  
 function GetController<T extends BaseController>(  
   controller: interfaces.ServiceIdentifier<T>): T {  
   return container.get<T>(controller);  
 }  
 export { GetObject, SetConstant, GetController, IsAlreadyInitilized };  

Helper items are singleton whereas controllers are scoped. Since we do not need to have multiple instances created for helpers wheres controllers has to re-initialize.

GetObject and GetController are used to get items which are already present in DI. The one item SetConstant is used to set a new item as a singleton. We would see the usage of same later. The GetController we have already seen how to use it. There are few Generic constraints set on them.

To enable classes to use DI, we need to set attribute @injectable() on those classes.

Controllers with handlebars as it's associated Views

Root Controller

 import { injectable } from "inversify";  
 import { HttpRequestResponse } from './../../Component/IndexInternal';  
 import { BaseController, IGeneralRouteController } from './../../Base/BaseController';  
 import { HomeViewHolderOption } from './HomeViewHolderOption';  
 var HomeView = require("./HomeView.handlebars");  
 @injectable()  
 class HomeController  
   extends BaseController  
   implements IGeneralRouteController {  
   constructor(public HolderOption: HomeViewHolderOption) {  
     super();  
   }  
   Init(routeNextWrapper?: (isInitialized?: boolean) => void) {  
     if (window.location.hash.slice(2) == '' ||  
       !$(this.HolderOption.ContainerSelector).data('child-init')) {  
       this.InitPage();  
     }  
     routeNextWrapper();  
   }  
   InitPage() {  
     $(this.HolderOption.ContainerSelector).html(HomeView);  
   }  
 }  
 export { HomeController }  

HomeView is holding up handlebars template which is rendered on InitPage function.

The condition that we are checking by URL and ContainerSelector to avoid re-render if any nested URL is requested. The child-init data attribute would be set as true by child controller.

The other thing to mark is HomeViewHolderOption, this is going to be initialized by a parent, in this situation, we have used with SetConstant while initializing master route.

    var app = SpaApp.App;   
    app.SetConstant(app.HomeViewHolderOption,   
     new app.HomeViewHolderOption({   
      ContainerSelector: '.app-main'   
     }));   

No, DOM element or selectors should be used on TS code level and ideally, above items should be initialized on handlebars where actual DOM elements exist.

The code structure for above:

 import { injectable } from 'inversify';  
 @injectable()  
 class HomeViewHolderOption  
   implements IHomeViewHolderOption {  
   constructor(option: IHomeViewHolderOption) {  
     this.ContainerSelector = option.ContainerSelector;  
   }  
   ContainerSelector: string;  
 }  
 interface IHomeViewHolderOption {  
   ContainerSelector: string;  
 }  
 export { HomeViewHolderOption, IHomeViewHolderOption }  

At this place, we are creating an interface and inheriting same to have easier access on TS class as option constructor variable, so that we get intellisense and use the same type to initialize the class.

View for above controller

 <div id="myCarousel" class="carousel slide" data-ride="carousel" data-interval="6000">  
   <ol class="carousel-indicators">  
     <li data-target="#myCarousel" data-slide-to="0" class="active"></li>  
     <li data-target="#myCarousel" data-slide-to="1"></li>  
     <li data-target="#myCarousel" data-slide-to="2"></li>  
     <li data-target="#myCarousel" data-slide-to="3"></li>  
   </ol>  
   <div class="carousel-inner" role="listbox">  
     .... // HTML only, check Github for full
   </div>  
 </div>  

Like I said earlier there is no need of model always, same in this case.

Now, we would see one other example of child Controller with form involved in it.

ContactController.ts

 import { injectable } from "inversify";  
 import { GetObject } from './../../../Infrastructure/IocConfig';  
 import { HttpRequestResponse } from './../../../Component/IndexInternal';  
 import { BaseController, IGeneralRouteController } from './../../../Base/BaseController';  
 import { AboutViewOption } from './../AboutViewOption';  
 import { ContactViewOption, IContactViewOption } from './ContactViewOption';  
 import { Form, JqueryEventHelper } from './../../../Component/IndexInternal';  
 var ContactView = require("./ContactView.handlebars");  
 @injectable()  
 class ContactController  
   extends BaseController  
   implements IGeneralRouteController {  
   constructor(public AboutViewOption: AboutViewOption,  
     readonly FormHelper: Form,  
     readonly EventHelper: JqueryEventHelper) {  
     super();  
   }  
   Init(routeNextWrapper?: (isInitialized?: boolean) => void) {  
     if (!$.trim($(this.AboutViewOption.ContactContainerSelector).html()).length) {  
       this.InitPage();  
     }  
     routeNextWrapper();  
   }  
   InitPage() {  
     $(this.AboutViewOption.ContactContainerSelector).html(ContactView({  
       Title: 'Contact',  
       Message: 'Your contact page.'  
     })).data('child-init', true);  
   }  
 }  
 export { ContactController }  

Check at this place where we not checking for URL but just for content initialization also for parent item with are setting true for child-init data attribute.

View of above (ContactView.handlebars)

 <hr />  
 <h2>{{Title}}</h2>  
 <h3>{{Message}}</h3>  
 <div class="container">  
   <div class="row">  
     <div class="col-md-8">  
       <div class="well well-sm">  
         <form id="ContactForm" method="post" action="/home/contact">  
           <div class="row">  
             <div class="col-md-6">  
               <div class="form-group">  
                 <label for="name">  
                   Name  
                 </label>  
                 <input type="text" class="form-control" id="Name" name="Name" placeholder="Enter name"  
                     data-val="true"  
                     data-val-length="The Name must be of 2 to 150 characters."  
                     data-val-length-max="150" data-val-length-min="2"  
                     data-val-required="Name is required."  
                     value="VK" />  
                 <div class="field-validation-error"  
                    data-valmsg-for="Name" data-valmsg-replace="true" id="NameError">  
                 </div>  
               </div>  
               <div class="form-group">  
                 <label for="email">  
                   Email Address  
                 </label>  
                 <div class="form-group">  
                   <div class="input-group">  
                     <span class="input-group-addon">  
                       <span class="glyphicon glyphicon-envelope"></span>  
                     </span>  
                     <input type="text" class="form-control" id="Email" placeholder="Enter email"  
                         name="Email"  
                         data-val="true"  
                         data-val-required="Email is required."  
                         data-val-regex="Invaild email id."  
                         data-val-regex-pattern="^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$"  
                         value="viku85@gmail.com" />  
                   </div>  
                   <div class="field-validation-error"  
                      data-valmsg-for="Email" data-valmsg-replace="true" id="EmailError">  
                   </div>  
                 </div>  
                 <div class="form-group">  
                   <label for="subject">  
                     Subject  
                   </label>  
                   <select id="Subject" name="Subject" class="form-control" required="required">  
                     <option value="service">General</option>  
                     <option value="suggestions">Suggestions</option>  
                     <option value="product">Product Support</option>  
                   </select>  
                 </div>  
               </div>  
             </div>  
             <div class="col-md-6">  
               <div class="form-group">  
                 <label for="name">  
                   Message  
                 </label>  
                 <textarea name="Message" id="Message" class="form-control" rows="9" cols="25" required="required"  
                      placeholder="Message"  
                      data-val="true"  
                      data-val-length="The Partner Name must be of 10 to 2000 characters."  
                      data-val-length-max="2000" data-val-length-min="10"  
                      data-val-required="Message is required.">All is well.</textarea>  
                 <div class="field-validation-error"  
                    data-valmsg-for="Message" data-valmsg-replace="true" id="MessageError">  
                 </div>  
               </div>  
               <div class="col-md-12">  
                 <button type="submit" class="btn btn-primary pull-right" id="ContactUsSubmit">  
                   Send Message  
                 </button>  
               </div>  
             </div>  
           </div>  
         </form>  
       </div>  
     </div>  
     <div class="col-md-4">  
       <legend><span class="glyphicon glyphicon-globe"></span> Our office</legend>  
       <address>  
         One Microsoft Way<br />  
         Redmond, WA 98052-6399<br />  
         <abbr title="Phone">P:</abbr>  
         425.555.0100  
       </address>  
       <address>  
         <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br />  
         <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>  
       </address>  
     </div>  
   </div>  
 </div>  
 <script type="text/javascript">  
   var app = SpaApp.App;  
   app.SetConstant(app.ContactViewOption,  
     new app.ContactViewOption({  
       FormSelector: '#ContactForm',  
       SubmitButtonSelector: '#ContactUsSubmit'  
     }));  
   app.GetObject(app.ContactForm);  
 </script>  

You can see how unobtrusive validations are constructed on all input elements based jQuery unobtrusive validation. The next thing to look out for initialization of ContactViewOption and initializing ContactForm.

ContactForm.ts

 import { injectable } from "inversify";  
 import { GetObject } from './../../../Infrastructure/IocConfig';  
 import { HttpRequestResponse } from './../../../Component/IndexInternal';  
 import { BaseController, IGeneralRouteController } from './../../../Base/BaseController';  
 import { AboutViewOption } from './../AboutViewOption';  
 import { ContactViewOption, IContactViewOption } from './ContactViewOption';  
 import { Form, JqueryEventHelper } from './../../../Component/IndexInternal';  
 @injectable()  
 class ContactForm {  
   constructor(  
     readonly ContactViewOption: ContactViewOption,  
     readonly FormHelper: Form,  
     readonly EventHelper: JqueryEventHelper) {  
     this.Init();  
   }  
   Init() {  
     this.EventHelper.RegisterClickEvent(  
       this.ContactViewOption.SubmitButtonSelector,  
       (evt, selector) => {  
         this.FormHelper.SubmitForm({  
           Source: {  
             ButtonEvent: evt  
           },  
           OnPostSuccessResult: (data) => {  
             console.log('Submitted successfully.');  
           }  
         });  
       });  
   }  
 }  
 export { ContactForm }  

This is a sample there could be many things done but having a simplified version. Also, you can see how simple it is to use form callbacks. If it is just about form than globally form element can be registered to handle post and getting the result.

Example of one component, form.ts

 import { injectable } from "inversify";  
 @injectable()  
 class Form {  
   constructor() {  
   }  
   SubmitForm(option: AjaxFormPostContent) {  
     var form: JQuery;  
     var buttonSource: JQuery;  
     var curEvent: JQuery.Event<HTMLElement, null>;  
     if (option.Source.ButtonEvent) {  
       buttonSource = $(option.Source.ButtonEvent.currentTarget);  
       form = buttonSource.closest("form");  
       curEvent = option.Source.ButtonEvent;  
     }  
     else if (option.Source.FormSubmitEvent) {  
       form = $(option.Source.FormSubmitEvent.currentTarget);  
       curEvent = option.Source.FormSubmitEvent;  
     }  
     curEvent.preventDefault();  
     var formId = $(form).attr('id');  
     if (!formId) {  
       console.error('form id not specified');  
       return;  
     }  
     var triggeredForm = "#" + formId;  
     if (!$(triggeredForm).attr('action').length) {  
       console.error('URL missing for form submit.');  
       return;  
     }  
     if (form.hasClass("loading")) {  
       curEvent.preventDefault();  
       return;  
     }  
     form.addClass("loading");  
     $.each($(form).find("button[type='submit']"), (index, btn) => {  
       $(btn).addClass("loading");  
     });  
     $.validator.unobtrusive.parse(`#${formId}`);  
     var serializedData = {};  
     $(triggeredForm).serializeArray()  
       .map((key) => { serializedData[key.name] = key.value; });  
     if (option.SerializeData != undefined) {  
       option.SerializeData(serializedData);  
     }  
     var formProgress = (state: boolean) => {  
       // TODO: Add progess status for form.  
     };  
     if ($(triggeredForm).valid()) {  
       formProgress(true);  
       $.ajax({  
         url: $(triggeredForm).attr('action'),  
         data: JSON.stringify(serializedData),  
         contentType: 'application/json',  
         method: 'post',  
         success: (data) => {  
           if (data != undefined && data != '') {  
             if (option.OnPostSuccess != undefined) {  
               option.OnPostSuccess(data);  
             }  
             if (option.OnPostSuccessResult) {  
               option.OnPostSuccessResult(data);  
               // TODO: Success notification  
             }  
             return;  
           }  
         },  
         error: (data, b, c) => {  
           if (data != undefined &&  
             data.responseJSON != undefined &&  
             data.status == 400) {  
             if (option.OnValidationFailure) {  
               option.OnValidationFailure(data.responseJSON);  
             }  
             if (option.OnValidationFailureMessageHandling == undefined) {  
               var message = '';  
               var propStrings = Object.keys(data.responseJSON);  
               $.each(propStrings, (errIndex, propString) => {  
                 var propErrors = data.responseJSON[propString];  
                 $.each(propErrors, (errMsgIndex, propError) => {  
                   message += propError;  
                 });  
                 message += '\n';  
                 $(`#${propString}Error`).html(message)  
                   .removeClass('field-validation-valid').addClass('field-validation-error');  
                 message = '';  
               });  
             }  
             else {  
               option.OnValidationFailureMessageHandling(data.responseJSON);  
             }  
             return;  
           }  
           if (option.OnFailure != undefined) {  
             option.OnFailure();  
           }  
           else {  
             // TODO: Failure notification  
           }  
         },  
         complete: () => {  
           formProgress(false);  
         }  
       });  
     }  
     form.removeClass("loading");  
     $.each($(form).find("button[type='submit']"), (index, btn) => {  
       $(btn).removeClass("loading");  
     });  
     // TODO: Reset loading state of buttons that are blocked in initial function start  
   }  
 }  
 interface AjaxFormPostContent {  
   Source: {  
     ButtonEvent?: JQuery.Event<HTMLElement, null>,  
     FormSubmitEvent?: JQuery.Event<HTMLElement, null>,  
   },  
   SerializeData?: (data) => void;  
   OnPostSuccess?: (data: string) => void;  
   OnFailure?: () => void;  
   OnPostSuccessResult?: (data: any) => void;  
   OnValidationFailure?: (data: JSON) => void;  
   OnValidationFailureMessageHandling?: (data: JSON) => void;  
 }  
 export { Form, AjaxFormPostContent }  

Tons of codes with various callbacks but for easier understanding just have a look on the associated interface that can give you a fair idea about it. It is going to fill up more space to explain all things, I will leave it to you to explore on it.

Enhancements

I understand there could be numerous enhancements, missing items, flaws but this shall give you better start if you are looking for building something from scratch for SPA app and want to control in your own way. 

I did add few more components in it, like pop-up, notification, feel free keep adding and using it. More of all Area, Controller, View, and routing are okay. These give details about all the feature that we discussed in beginning.

For me, I am working on these enhancements in next iteration
- Director with webpack integration
- Routing to pass values from route to controller, DirectorJS has few issue.
- Validation approach by using server-side Data Annotation or Fluent validation so that we write validation logic only at one place.

Please feel free to contribute on Github or ask any query.

Comments

Popular posts from this blog

Making FluentValidation compatible with Swagger including Enum or fixed List support

FluentValidation is not directly compatible with Swagger API to validate models. But they do provide an interface through which we can compose Swagger validation manually. That means we look under FluentValidation validators and compose Swagger validator properties to make it compatible. More of all mapping by reading information from FluentValidation and setting it to Swagger Model Schema. These can be done on any custom validation from FluentValidation too just that proper schema property has to be available from Swagger. Custom validation from Enum/List values on FluentValidation using FluentValidation.Validators; using System.Collections.Generic; using System.Linq; using static System.String; /// <summary> /// Validator as per list of items. /// </summary> /// <seealso cref="PropertyValidator" /> public class FixedListValidator : PropertyValidator { /// <summary> /// Gets the valid items /// <...

Elegantly dealing with TimeZones in MVC Core / WebApi

In any new application handling TimeZone/DateTime is mostly least priority and generally, if someone is concerned then it would be handled by using DateTime.UtcNow on codes while creating current dates and converting incoming Date to UTC to save on servers. Basically, the process is followed by saving DateTime to UTC format in a database and keep converting data to native format based on user region or single region in the application's presentation layer. The above is tedious work and have to be followed religiously. If any developer misses out the manual conversion, then that area of code/view would not work. With newer frameworks, there are flexible ways to deal/intercept incoming or outgoing calls to simplify conversion of TimeZones. These are steps/process to achieve it. 1. Central code for storing user's state about TimeZone. Also, central code for conversion logic based on TimeZones. 2. Dependency injection for the above class to ...

Handling JSON DateTime format on Asp.Net Core

This is a very simple trick to handle JSON date format on AspNet Core by global settings. This can be applicable for the older version as well. In a newer version by default, .Net depends upon Newtonsoft to process any JSON data. Newtonsoft depends upon Newtonsoft.Json.Converters.IsoDateTimeConverter class for processing date which in turns adds timezone for JSON data format. There is a global setting available for same that can be adjusted according to requirement. So, for example, we want to set default formatting to US format, we just need this code. services.AddMvc() .AddJsonOptions(options => { options.SerializerSettings.DateTimeZoneHandling = "MM/dd/yyyy HH:mm:ss"; });

Storing and restoring Kendo Grid state from Database

There is no any built in way to store entire grid state into database and restore back again with all filters, groups, aggregates, page and page size. At first, I was trying to restore only filters by looking through DataSourceRequest. DataSourceRequest is kind of communication medium between client and server for the operation we do on grid. All the request comes via DataSourceRequest. In previous approach, I was trying to store IFileDescriptor interface which come with class FileDescriptor by looping through filters and serializing into string for saving into database but this IFileDescriptor can also contain CompositeFilterDescriptor which can be nested in nested object which are very tricky to handle. So, I had decompiled entire Kendo.MVC library and found out that all Kendo MVC controls are derived from “JsonObject”. It is there own implementation with ”Serialize” abstract function and “ToJson” function. In controls they are overriding “Serialize” method which depicts t...

Trim text in MVC Core through Model Binder

Trimming text can be done on client side codes, but I believe it is most suitable on MVC Model Binder since it would be at one place on infrastructure level which would be free from any manual intervention of developer. This would allow every post request to be processed and converted to a trimmed string. Let us start by creating Model binder using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Threading.Tasks; public class TrimmingModelBinder : IModelBinder { private readonly IModelBinder FallbackBinder; public TrimmingModelBinder(IModelBinder fallbackBinder) { FallbackBinder = fallbackBinder ?? throw new ArgumentNullException(nameof(fallbackBinder)); } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bin...

LDAP with ASP.Net Identity Core in MVC with project.json

Lightweight Directory Access Protocol (LDAP), the name itself explain it. An application protocol used over an IP network to access the distributed directory information service. The first and foremost thing is to add references for consuming LDAP. This has to be done by adding reference from Global Assembly Cache (GAC) into project.json "frameworks": { "net461": { "frameworkAssemblies": { "System.DirectoryServices": "4.0.0.0", "System.DirectoryServices.AccountManagement": "4.0.0.0" } } }, These  System.DirectoryServices  and  System.DirectoryServices.AccountManagement  references are used to consume LDAP functionality. It is always better to have an abstraction for irrelevant items in consuming part. For an example, the application does not need to know about PrincipalContext or any other dependent items from those two references to make it extensible. So, we can begin wi...

Configuring Ninject, Asp.Net Identity UserManager, DataProtectorTokenProvider with Owin

It can be bit tricky to configure both Ninject and Asp.Net Identity UserManager if some value is expected from DI to configure UserManager. We will look into configuring both and also use OwinContext to get UserManager. As usual, all configuration need to be done on Startup.cs. It is just a convention but can be used with different name, the important thing is to decorate class with following attribute to make it Owin start-up: [assembly: OwinStartup(typeof(MyProject.Web.Startup))] Ninject configuration Configuring Ninject kernel through method which would be used to register under Owin. Startup.cs public IKernel CreateKernel() { var kernel = new StandardKernel(); try { //kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); // TODO: Put any other injection which are required. return kernel; } catch { kernel.Dispose(); thro...

Data seed for the application with EF, MongoDB or any other ORM.

Most of ORMs has moved to Code first approach where everything is derived/initialized from codes rather than DB side. In this situation, it is better to set data through codes only. We would be looking through simple technique where we would be Seeding data through Codes. I would be using UnitOfWork and Repository pattern for implementing Data Seeding technique. This can be applied to any data source MongoDB, EF, or any other ORM or DB. Things we would be doing. - Creating a base class for easy usage. - Interface for Seed function for any future enhancements. - Individual seed classes. - Configuration to call all seeds. - AspNet core configuration to Seed data through Seed configuration. Creating a base class for easy usage public abstract class BaseSeed<TModel> where TModel : class { protected readonly IMyProjectUnitOfWork MyProjectUnitOfWork; public BaseSeed(IMyProjectUnitOfWork MyProjectUnitOfWork) { ...

A wrapper implementation for Kendo Grid usage

A wrapper implementation for any heavily used item is always a good practice. Whatever is not written by us and used at a lot of places should be wrapped within specific functionality to keep it future proof and easily changeable. This also encourages DRY principle to keep our common setting at a central place. Kendo UI items are enormous in configuration, one of an issue I find people keep repeating codes for Kendo Grid configuration. They have built very flexible system to have any configuration, but in most of the cases, we do not need all of those complicated configuration. We would try to see a simpler configuration of same. The actual core implementation is bit complex, but we do not have to bother about it once done since the focus is just on usage only. I recommend doing this practice for as simple as jQuery events, form handling or as simple as any notification system. This just won't make things simple but makes codes much more manageable, easy understand, read or open f...

Using Redis distributed cache in dotnet core with helper extension methods

Redis cache is out process cache provider for a distributed environment. It is popular in Azure Cloud solution, but it also has a standalone application to operate upon in case of small enterprises application. How to install Redis Cache on a local machine? Redis can be used as a local cache server too on our local machines. At first install, Chocolatey https://chocolatey.org/ , to make installation of Redis easy. Also, the version under Chocolatey supports more commands and compatible with Official Cache package from Microsoft. After Chocolatey installation hit choco install redis-64 . Once the installation is done, we can start the server by running redis-server . Distributed Cache package and registration dotnet core provides IDistributedCache interface which can be overrided with our own implementation. That is one of the beauties of dotnet core, having DI implementation at heart of framework. There is already nuget package available to override IDistributedCache i...