Angular WebPack with jQWidgets and Angular Dev Server

This tutorial will show you how to use Angular WebPack along with the Angular Components by jQWidgets.
Please, follow the instructions below:


You can also download the final result.

What is Webpack?

Webpack is a powerful module bundler. A bundle is a JavaScript file that incorporates assets that belong together and should be served to the client in a response to a single file request. A bundle can include JavaScript, CSS styles, HTML, and almost any other kind of file. Webpack roams over your application source code, looking for import statements, building a dependency graph, and emitting one or more bundles. With plugins and rules, Webpack can preprocess and minify different non-JavaScript files such as TypeScript, SASS, and LESS files. You determine what Webpack does and how it does it with a JavaScript configuration file, webpack.config.js.

Entries and outputs

webpack.config.js

entry: {
    'app': './src/main.ts'
},
        

You supply Webpack with one or more entry files and let it find and incorporate the dependencies that radiate from those entries. The one entry point file in this example is the application's root file, src/main.ts:

Webpack inspects that file and traverses its import dependencies recursively.

src/main.ts

import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent { }

It sees that you're importing @angular/core so it adds that to its dependency list for potential inclusion in the bundle. It opens the @angular/core file and follows its network of import statements until it has built the complete dependency graph from main.ts down. Then it outputs these files to the app.js bundle file designated in configuration:

output: {
    filename: 'app.js'
}

This app.js output bundle is a single JavaScript file that contains the application source and its dependencies. You'll load it later with a script tag in the index.html.

Multiple bundles

You probably don't want one giant bundle of everything. It's preferable to separate the volatile application app code from comparatively stable vendor code modules. Change the configuration so that it has two entry points, main.ts and vendor.ts:
entry: {
  app: 'src/app.ts',
  vendor: 'src/vendor.ts'
},
output: {
  filename: '[name].js'
}

Webpack constructs two separate dependency graphs and emits two bundle files, one called app.js containing only the application code and another called vendor.js with all the vendor dependencies.

To tell Webpack what belongs in the vendor bundle, add a vendor.ts file that only imports the application's third-party modules: src/vendor.ts

    // Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
// RxJS
import 'rxjs';
// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ..

Loaders

Webpack can bundle any kind of file: JavaScript, TypeScript, CSS, SASS, LESS, images, HTML, fonts, whatever. Webpack itself only understands JavaScript files. Teach it to transform non-JavaScript file into their JavaScript equivalents with loaders. Configure loaders for TypeScript and CSS as follows.

rules: [
  {
    test: /\.ts$/,
    loader: 'awesome-typescript-loader'
  },
  {
    test: /\.css$/,
    loaders: 'style-loader!css-loader'
  }
]

When Webpack encounters import statements like the following, it applies the test RegEx patterns.

import { AppComponent } from './app.component.ts';
import 'uiframework/dist/uiframework.css';
        

When a pattern matches the filename, Webpack processes the file with the associated loader. The first import file matches the .ts pattern so Webpack processes it with the awesome-typescript-loader. The imported file doesn't match the second pattern so its loader is ignored. The second import matches the second .css pattern for which you have two loaders chained by the (!) character. Webpack applies chained loaders right to left. So it applies the css loader first to flatten CSS @import and url(...) statements. Then it applies the style loader to append the css inside <style> elements on the page.

Plugins

Webpack has a build pipeline with well-defined phases. Tap into that pipeline with plugins such as the uglify minification plugin:
 plugins: [
  new webpack.optimize.UglifyJsPlugin()
]

Configuring Webpack

After that brief orientation, you are ready to build your own Webpack configuration for Angular apps. Begin by setting up the development environment. Create a new project folder.

mkdir angular-webpack
cd     angular-webpack

Add these files:

package.json

{
  "name": "angular2-webpack",
  "version": "1.0.0",
  "description": "A webpack starter for Angular",
  "scripts": {
    "start": "webpack-dev-server --inline --progress --port 8080",
    "test": "karma start",
    "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  },
  "license": "MIT",
  "dependencies": {
    "@angular/common": "~4.2.0",
    "@angular/compiler": "~4.2.0",
    "@angular/core": "~4.2.0",
    "@angular/forms": "~4.2.0",
    "@angular/http": "~4.2.0",
    "@angular/platform-browser": "~4.2.0",
    "jqwidgets-scripts": "~5.6.0",
    "@angular/platform-browser-dynamic": "~4.2.0",
    "@angular/router": "~4.2.0",
    "core-js": "^2.4.1",
    "rxjs": "5.0.1",
    "zone.js": "^0.8.4"
  },
  "devDependencies": {
    "@types/node": "^6.0.45",
    "@types/jasmine": "2.5.36",
    "angular2-template-loader": "^0.6.0",
    "awesome-typescript-loader": "^3.0.4",
    "css-loader": "^0.26.1",
    "extract-text-webpack-plugin": "2.0.0-beta.5",
    "file-loader": "^0.9.0",
    "html-loader": "^0.4.3",
    "html-webpack-plugin": "^2.16.1",
    "jasmine-core": "^2.4.1",
    "karma": "^1.2.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-jasmine": "^1.0.2",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^2.0.1",
    "null-loader": "^0.1.1",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.2",
    "style-loader": "^0.13.1",
    "typescript": "~2.3.1",
    "webpack": "2.2.1",
    "webpack-dev-server": "2.4.1",
    "webpack-merge": "^3.0.0"
  }
}

webpack.config.js

  module.exports = require('./config/webpack.prod.js');

config/helpers.js

var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
  args = Array.prototype.slice.call(arguments, 0);
  return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

config/karma.conf.js

var webpackConfig = require('./webpack.test');
module.exports = function (config) {
  var _config = {
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      {pattern: './config/karma-test-shim.js', watched: false}
    ],
    preprocessors: {
      './config/karma-test-shim.js': ['webpack', 'sourcemap']
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      stats: 'errors-only'
    },
    webpackServer: {
      noInfo: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['Chrome'],
    singleRun: true
  };
  config.set(_config);
};

config/karma-test-shim.js

Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var appContext = require.context('../src', true, /\.spec\.ts/);
appContext.keys().forEach(appContext);
var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());

Common Configuration

Developers typically have separate configurations for development, production, and test environments. All three have a lot of configuration in common. Gather the common configuration in a file called webpack.common.js.

config/webpack.common.js

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
module.exports = {
  entry: {
    'polyfills': './src/polyfills.ts',
    'vendor': './src/vendor.ts',
    'app': './src/main.ts'
  },
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: helpers.root('src', 'tsconfig.json') }
          } , 'angular2-template-loader'
        ]
      },
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'file-loader?name=assets/[name].[hash].[ext]'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('src', 'app'),
        loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' })
      },
      {
        test: /\.css$/,
        include: helpers.root('src', 'app'),
        loader: 'raw-loader'
      }
    ]
  },
  plugins: [
    // Workaround for angular/angular#11580
    new webpack.ContextReplacementPlugin(
      // The (\\|\/) piece accounts for path separators in *nix and Windows
      /angular(\\|\/)core(\\|\/)@angular/,
      helpers.root('./src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.optimize.CommonsChunkPlugin({
      name: ['app', 'vendor', 'polyfills']
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html'
    })
  ]
};

Inside webpack.common.js
Webpack is a NodeJS-based tool that reads configuration from a JavaScript commonjs module file.

The configuration imports dependencies with require statements and exports several objects as properties of a module.exports object.

entry—the entry-point files that define the bundles.
resolve—how to resolve file names when they lack extensions.
module.rules— module is an object with rules for deciding how files are loaded.
plugins—creates instances of the plugins.

Development configuration

config/webpack.dev.js

var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
  devtool: 'cheap-module-eval-source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: '/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },
  plugins: [
    new ExtractTextPlugin('[name].css')
  ],
  devServer: {
    historyApiFallback: true,
    stats: 'minimal'
  }
});

Production configuration

config/webpack.prod.js

var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: '/',
    filename: '[name].[hash].js',
    chunkFilename: '[id].[hash].chunk.js'
  },
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
      mangle: {
        keep_fnames: true
      }
    }),
    new ExtractTextPlugin('[name].[hash].css'),
    new webpack.DefinePlugin({
      'process.env': {
        'ENV': JSON.stringify(ENV)
      }
    }),
    new webpack.LoaderOptionsPlugin({
      htmlLoader: {
        minimize: false // workaround for ng2
      }
    })
  ]
});

config/webpack.test.js

var webpack = require('webpack');
var helpers = require('./helpers');
module.exports = {
  devtool: 'inline-source-map',
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loaders: [
          {
            loader: 'awesome-typescript-loader',
            options: { configFileName: helpers.root('src', 'tsconfig.json') }
          } , 'angular2-template-loader'
        ]
      },
      {
        test: /\.html$/,
        loader: 'html-loader'
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'null-loader'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('src', 'app'),
        loader: 'null-loader'
      },
      {
        test: /\.css$/,
        include: helpers.root('src', 'app'),
        loader: 'raw-loader'
      }
    ]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      // The (\\|\/) piece accounts for path separators in *nix and Windows
      /angular(\\|\/)core(\\|\/)@angular/,
      helpers.root('./src'), // location of your src
      {} // a map of your routes
    )
  ]
}

Polyfills

You'll need polyfills to run an Angular application in most browsers. Polyfills should be bundled separately from the application and vendor bundles. Add a polyfills.ts like this one to the src/ folder.
import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
  // Production
} else {
  // Development and test
  Error['stackTraceLimit'] = Infinity;
  require('zone.js/dist/long-stack-trace-zone');
}

Index

src/index.html

<!DOCTYPE html>  
 <html>  
  <head>  
   <base href="/">  
   <title>Angular With Webpack and jQWidgets</title>  
   <!-- Styles -->  
      <link href="../node_modules/jqwidgets-scripts/jqwidgets/styles/jqx.base.css" type="text/css" rel="stylesheet" />       
   <meta charset="UTF-8">  
   <meta name="viewport" content="width=device-width, initial-scale=1">  
  </head>  
  <body>  
   <my-app>Loading...</my-app>  
  </body>  
 </html>  

src/main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app/app.module';
if (process.env.ENV === 'production') {
  enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

src/vendor.ts

 // Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
 
// RxJS
import 'rxjs';
 
// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ...

src/tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
	 "typeRoots": [
      "../node_modules/@types/"
    ]
  }
}

src/assets/css/styles.css

body {
    background: #0147A7;
    color: #fff;
}

src/assets/images/angular.png


src/app/app.component.html

<main>  
  <h1>Hello from Angular App with Webpack</h1>  
  <img src="../assets/images/angular.png">  
   <jqxBarGauge   
       [width]="600" [height]="600" [colorScheme]="'scheme02'"  
       [max]="150" [values]="values" [tooltip]="tooltip">  
      </jqxBarGauge>   
 </main> 

src/app/app.component.css

 main {
  padding: 1em;
  font-family: Arial, Helvetica, sans-serif;
  text-align: center;
  margin-top: 50px;
  display: block;
}

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
 
import { AppComponent } from './app.component';
import { jqxBarGaugeModule } from 'jqwidgets-ng/jqxbargauge';
@NgModule({
  imports: [
    BrowserModule, jqxBarGaugeModule
  ],
  declarations: [
    AppComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
}

src/app/app.component.ts

import { Component } from '@angular/core';
import '../assets/css/styles.css';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  tooltip: any =
    {
        visible: true,
        formatFunction: (value: string): string => {
            const realVal = parseInt(value);
            return 'Year: 2016
Price Index:' + realVal; } }; values: number[] = [102, 115, 130, 137]; }

src/app/app.component.spec.ts

import { TestBed } from '@angular/core/testing';
 
import { AppComponent } from './app.component';
 
describe('App', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({ declarations: [AppComponent]});
  });
 
  it ('should work', () => {
    let fixture = TestBed.createComponent(AppComponent);
    expect(fixture.componentInstance instanceof AppComponent).toBe(true, 'should create AppComponent');
  });
});

Your folder structure should look like below:


 /root
    /src
        /app
                app.component.css
                app.component.html
                app.component.ts
                app.component.spec.ts
                app.module.ts
        /assets
            /css
                styles.css
            /images
                angular.png
        index.html
        main.ts
        polyfills.ts
        tsconfig.json
        vendor.ts
    package.json
    webpack.config.js
    /config
        karma.conf.js
        helpers.js      
        karma-test-shim.js      
        webpack.common.js    
        webpack.dev.js     
        webpack.prod.js      
        webpack.test.js     
            

Install the NPM packages

npm install

Start the Application

npm start

Open a web browser and type: http://localhost:8080/

The result is: