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.

Popular posts from this blog

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



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 be able to use globally. 3. Creating Mo…

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 with some bas…

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(bindingC…

Architecture solution composting Repository Pattern, Unit Of Work, Dependency Injection, Factory Pattern and others

Project architecture is like garden, we plant the things in certain order and eventually they grow in similar manner. If things are planted well then they will all look(work) great and easier to manage. If they grow as cumbersome it would difficult to maintain and with time more problems would be happening in maintenance.

There is no any fixed or known approach to decide project architecture and specially with Agile Methodology. In Agile Methodology, we cannot predict how our end products will look like similarly we cannot say a certain architecture will fit well for entire development lifespan for project. So, the best thing is to modify the architecture as per our application growth. I understand that it sounds good but will be far more problematic with actual development. If it is left as it is then more problems will arise with time. Just think about moving plant vs a full grown tree.

Coming to technical side, In this article, I will be explaining about the various techniques tha…

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

Kendo MVC Grid DataSourceRequest with AutoMapper - Advance

The actual process to make DataSourceRequest compatible with AutoMapper was explained in my previous post Kendo MVC Grid DataSourceRequest with AutoMapper, where we had created custom model binder attribute and in that property names were changed as data models.

In this post we will be looking into using AutoMapper's Queryable extension to retrieve the results based on selected columns. When Mapper.Map<RoleViewModel>(data) is called it retrieves all column values from table. The Queryable extension provides a way to retrieve only selected columns from table. In this particular case based on properties of RoleViewModel.
The previous approach that we implemented is perfect as far as this article (3 Tips for Using Telerik Data Access and AutoMapper) is concern about performance where it states: While this functionality allows you avoid writing explicit projection in to your LINQ query it has the same fatal flaw as doing so - it prevents the query result from being cached.
Since …

Kendo MVC Grid DataSourceRequest with AutoMapper

Kendo Grid does not work directly with AutoMapper but could be managed by simple trick using mapping through ToDataSourceResult. The solution works fine until different filters are applied.
The problems occurs because passed filters refer to view model properties where as database model properties are required after AutoMapper is implemented.
So, the plan is to intercept DataSourceRequest  and modify names based on database model. To do that we are going to create implementation of CustomModelBinderAttribute to catch calls and have our own implementation of DataSourceRequestAttribute from Kendo MVC. I will be using same source code from Kendo but will replace column names for different criteria for sort, filters, group etc.
Let's first look into how that will be implemented.
public ActionResult GetRoles([MyDataSourceRequest(GridId.RolesUserGrid)] DataSourceRequest request) { if (request == null) { throw new ArgumentNullException("reque…

Global exception handling and custom logging in AspNet Core with MongoDB

In this, we would be looking into logging and global exception handling in the AspNet Core application with proper registration of logger and global exception handling.

Custom logging
The first step is to create a data model that we want to save into DB.

Error log Data model
These are few properties to do logging which could be extended or reduced based on need.

public class ErrorLog { /// <summary> /// Gets or sets the Error log identifier. /// </summary> /// <value> /// The Error log identifier. /// </value> [BsonRepresentation(BsonType.ObjectId)] public ObjectId Id { get; set; /// <summary> /// Gets or sets the date. /// </summary> /// <value> /// The date. /// </value> public DateTime Date { get; set; } /// <summary> /// Gets or sets the thread. /// </summary> /// <v…

T4, Generating interface automatically based on provided classes

With new techniques and patterns interface plays a key role in application architecture. Interface makes application extendable like defining file upload interface and implementing based on file system, Azure Blob storage, Amazon S3. At starting we might be implementing based on Azure Blob but later we might move to Windows based file system and so on.

Ideally we create interface based on need and start implementing actual default implementation class. Many a times at starting of implementation there is one to one mapping between Interface and Class. Like from above example File upload interface and the initial or default class implementation that we design and with time it will get extended.
In this article, we will try to create interface based on default class implementation. This is not at all recommended in Test Driven Design (TDD) where we test the application before actual code implementation but I feel sometimes and in some situations it is okay do that and test straight afte…