Bundling ToDoMVC with WebPack

Source code

We now have the frontend moved out of the back end, served by a static web server at a different URL. Great! Let’s use Webpack to bundle our JavaScript and run under its development server, giving us hands-free browser reloading.

Webpack gives us browser-side module loading, so we’ll switch app.js and todo.js to use CommonJS modules.

Steps

  1. From the previous step, make a virtual env if necessary, then install if needed the npm and Python dependencies.

  2. Hook up ESLint in preferences if necessary.

  3. Install our new dependencies:

    npm install --save-dev webpack webpack-dev-server
    
  4. Update our HTML file, replacing <script> nodes for jquery.js, app.js, and todo.js with a single bundle.js:

    ToDo Webpack index.html
    <!DOCTYPE html>
    <html>
    <head>
        <title>Flask ToDoMVC</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css"/>
        <link rel="stylesheet" href="app.css"/>
        <link rel="icon" type="image/x-icon" href="favicon.ico"/>
    </head>
    <body class="container">
    <div class="col-md-6">
        <h1>ToDos</h1>
        <p><input id="newName" placeholder="Add todo..." class="form-control"/></p>
    
        <div id="todoList">
            <ul></ul>
        </div>
    </div>
    <script src="bundle.js"></script>
    <script type="text/html" id="list_todos">
        <ul class="list-group">
            <% for ( var i = 0; i < todos.length; i++ ) { %>
            <li class="list-group-item" id="<%= todos[i].id %>">
                <span><%= todos[i].name %></span>
                <input class="form-control input-sm" title="Edit title"
                       value="<%= todos[i].name %>"/>
    
                <div class="btn-group pull-right" role="group">
                    <button class="btn btn-xs btn-default edit">Edit</button>
                    <button class="btn btn-xs btn-default delete">Delete</button>
                </div>
            </li>
            <% } %>
        </ul>
    </script>
    </body>
    </html>
    
  5. tmpl.js simply moves the template code out of app.js, into an export:

    ToDo Webpack tmpl.js
    // John Resig jQuery Microtemplating
    
    /*eslint-disable */
    module.exports = function tmpl (str, data) {
        // Figure out if we're getting a template, or if we need to
        // load the template - and be sure to cache the result.
        var cache = {};
    
        var fn = !/\W/.test(str) ?
            cache[str] = cache[str] ||
                tmpl(document.getElementById(str).innerHTML) :
    
            // Generate a reusable function that will serve as a template
            // generator (and which will be cached).
            new Function("obj",
                "var p=[],print=function(){p.push.apply(p,arguments);};" +
    
                    // Introduce the data as local variables using with(){}
                "with(obj){p.push('" +
    
                    // Convert the template into pure JavaScript
                str
                    .replace(/[\r\t\n]/g, " ")
                    .split("<%").join("\t")
                    .replace(/((^|%>)[^\t]*)'/g, "$1\r")
                    .replace(/\t=(.*?)%>/g, "',$1,'")
                    .split("\t").join("');")
                    .split("%>").join("p.push('")
                    .split("\r").join("\\'")
                + "');}return p.join('');");
    
        // Provide some basic currying to the user
        return data ? fn(data) : fn;
    };
    /*eslint-enable */
    
  6. app.js gets rid of the IIFE, imports jQuery, and imports todo.js:

    ToDo Webpack app.js
    var $ = require('jquery'),
        ToDos = require('./todo');
    
    $(document).ready(function () {
    
        // All REST requests should send content type, and log failures
        $.ajaxSetup({contentType: 'application/json'});
        $(document).ajaxError(function (event, jqxhr, settings, thrownError) {
            console.error('Ajax call failed:',
                settings.type, settings.url, thrownError);
        });
    
    });
    
  7. todo.js also gets rid of the IIFE. It does not do module.exports, which seems kind of weird. But that’s because it does its work on jQuery $(document).ready and we don’t want to refactor too much in this step. It does, though, import jQuery and tmpl.js at the top:

    ToDo Webpack todo.js
    var $ = require('jquery'),
        tmpl = require('./tmpl');
    
    $(document).ready(function () {
    
        var newName = $('#newName'),
            todoList = $('#todoList'),
            template = tmpl('list_todos');
    
        var todos;
    
        function refreshToDos () {
            /* Fetch the list of todos and re-draw the listing */
            $.get('http://localhost:5000/api/todo', function (data) {
                todos = data['objects'];
                todoList.find('ul')
                    .replaceWith(template({todos: todos}));
            });
        }
    
        // Create a new to do
        newName.change(function () {
            var payload = JSON.stringify({name: newName.val()});
            $.post('http://localhost:5000/api/todo', payload, function () {
                refreshToDos();
                newName.val('');
            })
        });
    
        // Edit a to do
        todoList.on('click', '.edit', function () {
            // Toggle the <input> for this todo
            todoList.find('li').removeAttr('editing');
            var li = $(this).closest('li').first().attr('editing', '1');
        });
        todoList.on('change', 'input', function () {
            // When the revealed-input changes, update using PATCH
            var todoId = $(this).closest('li')[0].id,
                data = JSON.stringify({name: $(this).val()});
            $.ajax({url: 'http://localhost:5000/api/todo/' + todoId, type: 'PATCH', data: data})
                .done(function () {
                    refreshToDos();
                });
        });
    
        // Delete an existing to do
        todoList.on('click', '.delete', function () {
            var todoId = $(this).closest('li')[0].id;
            $.ajax({url: 'http://localhost:5000/api/todo/' + todoId, type: 'DELETE'})
                .done(function () {
                    refreshToDos();
                });
        });
    
        // On startup, go fetch the list of todos and re-draw
        refreshToDos();
    });
    
  8. Let’s add a line to package.json‘s section for scripts, to start the dev server, based in app/:

    ToDo Webpack package.json
    {
      "name": "flask_todo",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "directories": {
        "doc": "doc"
      },
      "scripts": {
        "lint": "eslint todo/static/*.js",
        "start": "webpack-dev-server --content-base app/"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "jquery": "^2.2.0"
      },
      "devDependencies": {
        "eslint": "^1.10.3",
        "webpack": "^1.12.12",
        "webpack-dev-server": "^1.14.1"
      }
    }
    
  9. We need a (simple) webpack.config.js to drive the dev server:

    ToDo Webpack webpack.config.js
    module.exports = {
        context: __dirname + '/app',
        entry: './app.js',
        devtool: 'source-map'
    };
    
  10. Start Flask. Right-click on run_server.py and run it.

  11. Run dev server. Use PyCharm’s npm run too window to run start, then visit http://localhost:5000/.

And now we’re in business! To emphasize the workflow, let’s resize PyCharm and Chrome so they can be side-by-side. As you type, you will see the updates automatically.