Tuesday, 24 May 2016

The Mysterious Case of Webpack, Angular and jQuery

You may know that Angular ships with a cutdown version of jQuery called jQLite. It's still possible to use the full-fat jQuery; to quote the docs:

To use jQuery, simply ensure it is loaded before the angular.js file.

Now the wording rather implies that you're not using any module loader / bundler. Rather that all files are being loaded via script tags and relies on the global variables that result from that. True enough, if you take a look at the Angular source you can see how this works:


  // bind to jQuery if present;
  var jqName = jq();
  jQuery = isUndefined(jqName) ? window.jQuery :   // use jQuery (if present)
           !jqName             ? undefined     :   // use jqLite
                                 window[jqName];   // use jQuery specified by `ngJq`

Amongst other things it looks for a jQuery variable which has been placed onto the window object. If it is found then jQuery is used; if it is not then it's jqLite all the way.

But wait! I'm using webpack

Me too! And one of the reasons is that we get to move away from reliance upon the global scope and towards proper modularisation. So how do we get Angular to use jQuery given the code we've seen above? Well, your first thought might be to npm install yourself some jQuery and then make sure you've got something like this in your entry file:


import "jquery"; // This'll fix it... Right?
import * as angular from "angular";

Wrong.

You need the ProvidePlugin

In your webpack.config.js you need to add the following entry to your plugins:


      new webpack.ProvidePlugin({
          "window.jQuery": "jquery"
      }),

This uses the webpack ProvidePlugin and, at the point of webpackification (© 2016 John Reilly) all references in the code to window.jQuery will be replaced with a reference to the webpack module that contains jQuery. So when you look at the bundled file you'll see that the code that checks the window object for jQuery has become this:


  jQuery = isUndefined(jqName) ? __webpack_provided_window_dot_jQuery :   // use jQuery (if present)
           !jqName             ? undefined     :   // use jqLite
                                 window[jqName];   // use jQuery specified by `ngJq`

That's right; webpack is providing Angular with jQuery whilst still not placing a jQuery variable onto the window. Neat huh?

Friday, 13 May 2016

Inlining Angular Templates with WebPack and TypeScript

This technique actually applies to pretty much any web stack where you have to supply templates; it just so happens that I'm using Angular 1.x in this case. Also I have an extra technique which is useful to handle the ng-include scenario.

Preamble

For some time I've been using webpack to bundle my front end. I write ES6 TypeScript; import statements and all. This is all sewn together using the glorious ts-loader to compile and emit ES6 code which is handed off to the wonderful babel-loader which transpiles it to ESold code. All with full source map support. It's wonderful.

However, up until now I've been leaving Angular to perform the relevant http requests at runtime when it needs to pull in templates. That works absolutely fine but my preference is to preload those templates. In fact I've written before about using the gulp angular template cache to achieve just that aim.

So I was wondering; in this modular world what would be the equivalent approach? Sure I could still use the gulp angular template cache approach but I would like something a little more deliberate and a little less magic. Also, I've discovered (to my cost) that when using the existing approach, it's possible to break the existing implementation without realising it; only finding out there's a problem in Production when unexpected http requests start happening. Finding these problems out at compile time rather than runtime is always to be strived for. So how?

raw-loader!

raw-loader allows you load file content using require statements. This works well with the use case of inlining html. So I drop it into my webpack.config.js like so:


var path = require('path');

module.exports = {
    cache: true,
    entry: {
        main: './src/main.ts',

        vendor: [
          'babel-polyfill',
          'angular',
          'angular-animate',
          'angular-sanitize',
          'angular-ui-bootstrap',
          'angular-ui-router'
        ]
    },
    output: {
        path: path.resolve(__dirname, './dist/scripts'),
        filename: '[name].js',
        chunkFilename: '[chunkhash].js'
    },
    module: {
        loaders: [{
            test: /\.ts(x?)$/,
            exclude: /node_modules/,
            loader: 'babel-loader?presets[]=es2015!ts-loader'
        }, {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {
                presets: ['es2015']
            }
        }, { // THIS IS THE MAGIC!
            test: /\.html$/,
            exclude: /node_modules/,
            loader: 'raw'
        }]  // THAT WAS THE MAGIC!
    },
    plugins: [
      // ....
    ],
    resolve: {
        extensions: ['', '.ts', '.tsx', '.js']
    }
};

With this in place, if someone requires a file with the html suffix then raw-loader comes in. So now we can swap this:


  $stateProvider
    .state('state1', {
      url: "/state1",
      templateUrl: "partials/state1.html"
    })

For this:


  $stateProvider
    .state('state1', {
      url: "/state1",
      template: require("./partials/state1.html")
    })

Now initially TypeScript is going to complain about your require statement. That's fair; outside of node-land it doesn't know what require is. No bother, you just need to drop in a one line simple definition file to sort this out; let me present webpack-require.d.ts:


declare var require: (filename: string) => any;

You've now inlined your template. And for bonus points, if you were to make a mistake in your path then webpack would shout at you at compile time; which is a good, good thing.

ng-include

The one use case that this doesn't cover is where your templates import other templates through use of the ng-include directive. They will still trigger http requests as the templates are served. The simple way to prevent that is by priming the angular $templateCache like so:


  app.run(["$templateCache",
    ($templateCache: ng.ITemplateCacheService) => {
        $templateCache.put("justSome.html", require("./justSome.html"));
        // Other templates go here...
    }]);

Now when the app spins up it already has everything it needs pre-cached.