21 Aug 2022 ~ 8 min read

Webpack, is it really legacy - Part 02

Building a SPA using webpack.

Listen to something with me 😍

Want to listen somewhere else? 🎧

Table of Contents

Preface

From our previous article on how to configure webpack, check it out if you haven’t, we started with a simple project and configured webpack to provide us with a dev server, building and using a template for our base html. But it looked like it was from the era of the internets inception. We had to style it somehow, on this article we are going to be doing exactly that. Let’s go ahead and do that

Top

Our Directory Structure

So previously we had a directory structure where we had a public directory to put our base HTML template file. Now we’re also adding our sass files there.

webstorm
   |- configs # empty for now
   |- dist # output's will be here.
   |- public
      |- index.html
      |- style.scss     
   |- src
      |- index.js
   |- .gitignore
   |- package.json
   |- webpack.config.js

Top

Adding Sass and CSS loaders

Since we’ll be working with styles in this article, let’s install webpack’s sass and css loaders. There are a couple of loaders and packages to install,

  1. sass: The packages that the webpack loader uses to build(change) sass files into css files.
  2. css-loader: This is a webpack loader that is used to interpret @import and url()s. What that means when webpack encounters these it will pass it to the css-loader and it will convert them if necessary to be usable as an output inside HTML and CSS files.
  3. sass-loader: We use this package to handle converting sass files into css using the sass package mentioned above.
  4. MiniCssExtractPlugin: This will extract CSS files per JS import it encounters, however one of it’s features it won’t build duplicates.

So, what is a webpack loader? A loader is described as so in the webpack documentation

Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you import or “load” them. (Webpack Docs)

But to elaborate more, think of them as factory conveyer belt thing. As you encounter a loaded module(file) of a certain type it will go through a sequence of loaders and finally you get some sort of output. A perfect example is converting sass into css, I’ll explain in the next session. Now let’s install them into our project.

# Install them as devDependencies
npm install -D sass \
        sass-loader \
        css-loader \
        mini-css-extract-plugin

Top

Configuring webpack to load Sass files

Let’s configure webpack to load our sass files, this is what our config should look something like this.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const rootDir = path.resolve(__dirname, "..");
const PORT = process.env.PORT || 8080;

module.exports = {
  entry: "./src/index.js",
  mode: "development",
  output: {
    filename: "main.js",
    path: path.resolve(rootDir, "dist"),
    clean: true, // Delete old build before each build
  },
  resolve: {
    alias: {
      public: "/public", // import's will `public` point to this directory
    }
  },
  devServer: {
    port: PORT,
    host: "0.0.0.0",
    watchFiles: ["src/**/*", "public/**/*"],
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass|css)$/i, // check for these files
        // Use these loaders for processing them
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(rootDir, "public", "index.html"),
      inject: "body",
    }),
    new MiniCssExtractPlugin({
      filename: "style.css" // Output file name of the css bundle
    }),
  ],
};

Let’s go through important one’s one by one, first is straight forward. We’re telling webpack to clean before each build.

output: {
  filename: "main.js",
  path: path.resolve(rootDir, "dist"),
  clean: true, // Delete old build before each build
},

Second one is the rules section, so rules are where you tell webpack how a file type should be handled. So here we’re telling webpack if it finds any files of ending with the extension sass, scss or css it should use the loaders defined in the exact order they are put in.

module: {
  rules: [
    {
      test: /\.(scss|sass|css)$/i, // check for these files
      // Use these loaders for processing them
      use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 
    },
  ],
},

One thing to note here is order matters, remember I took the analogy of a conveyer belt because it is exactly that. The order however goes from last loader to the first loader. In this case, the input first goes through the sass loader then it is passed to the css loader after the css loader parses and changes any @import or url statements finally it goes to the mini-css-extract-plugin.

Finally, there is the plugins section. This section here will be used to configure the plugins, since the mini-css-extract-plugin is also a plugin you can configure the plugin in that specific section. Here we’re simply telling it to output the file in that specific style.css name.

plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(rootDir, "public", "index.html"),
    inject: "body",
  }),
  new MiniCssExtractPlugin({
    filename: "style.css" // Output file name of the css bundle
  }),
],

Top

Adding Some HTML

Let’s improve the HTML a bit more, so we can style it using scss, this is what I have it for

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Webstorm</title>
</head>

<body>
    <div id="container">
        <section>
            <h4>Tell me an idea</h4>
            <textarea name="idea" id="idea" autofocus placeholder="Start Writing Here"></textarea>

            <div id="controls">
                <button id="add-note-btn">Add Note</button>

                <small>To add a note press here <code>SHIFT</code> + <code>ENTER</code>.</small>
            </div>
        </section>

        <section>
            <h4>Brainstormed Ideas</h4>
            <hr>
            <div id="brainstormed"></div>
        </section>
    </div>
</body>

</html>

Top

Adding Some Styling

Since this is not a css styling article, I’ll just paste the sass I made here so you can simply copy and use it.

// public/style.scss
@import url('https://fonts.googleapis.com/css2?family=Aboreto&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,200;0,400;1,200&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Aboreto&display=swap');

$text-color: #000000;
$color: #000000;
$bg: #f5f5f5;
$shadow-color: #b3b3b3;
$shadow: 4px 4px 1px $shadow-color;
$monofont: "Roboto", monospace;
$displaymonofont: "Aboreto", monospace;

* {
    margin: 0;
    padding: 0;
}

html {
    height: 100%;
}

body {
    font-family: helvetia, arial;
    width: 100%;
    height: 100%;
}

#container {
    display: flex;
    flex-direction: row;
    background: $bg;
    height: 100%;

    &>* {
        width: 100%;
        height: 100%;
    }

    section {
        padding: .2em;
        box-sizing: border-box;
        overflow-y: auto;

        hr {
            border: 0;
            height: 0;
            background: $text-color;
            border-top: 1px solid $color;
            border-bottom: 1px solid $color;
        }

        h4 {
            font-family: "Aboreto", serif;
            font-size: 2.0em;
            margin-bottom: .4em;
            color: $text-color;
        }

        textarea {
            width: 100%;
            height: 80%;
            border-style: solid;
            border-width: 0.1em;
            border-color: $color;
            box-sizing: border-box;
            padding: 0.2em;
            background-color: inherit;
            outline: none;

            font-family: $monofont;
            font-size: 1.2rem;
            font-weight: normal;
            color: $text-color;

            &:focus {
                border-width: 0.2em;
                padding: 0.1em;
                border-color: $color;
            }

            &::placeholder {
                font-family: $monofont;
                color: $text-color;
                font-weight: 200;
            }
        }

        #controls {
            display: flex;
            flex-direction: row;
            justify-content: space-between;
        }

        code {
            color: $bg;
            background-color: $text-color;
            padding: 4px 8px;
            border-radius: 4px;
            font-family: $monofont;
            font-size: 0.8em;
        }

        button {
            background-color: $text-color;
            color: $bg;
            padding: 4px 8px;
            box-shadow: $shadow;
            transition-duration: 120ms;
            transition-property: box-shadow, color;
            transition-timing-function: ease-in-out;
            cursor: pointer;

            $border-width: 1px;
            $active-shadow: 0 0 1px $shadow-color;

            &:hover {
                box-shadow: $active-shadow;
            }

            &:active {
                box-shadow: $active-shadow;
                border: $border-width solid $text-color;
                padding: calc(4px + $border-width) calc(8px + $border-width);
                background: $bg;
                color: $text-color;
            }
        }

        #brainstormed {
            display: flex;
            flex-direction: column;
            gap: 0.5em;
            padding: .8em .8em 0 .8em;

            .idea-card {
                color: $bg;
                background-color: $color;
                font-family: $displaymonofont;

                box-shadow: $shadow;
                padding: .4em;
                line-height: 1.9em;
            }
        }
    }
}

One thing I can mention from this is, you can see I am using an @import directive here. These type’s of directives are going to get handled by the css-loader.

Top

Adding Some JavaScript into the mix

For our JavaScript it’s a simple change, we’ll simply change our javascript so we can use the Add Note button to add an one of our brainstormed ideas. Change the index.js file like so.

const showdown = require("showdown");
const converter = new showdown.Converter();

import "public/style.scss";

/** @param {String} value */
const addNote = (value) => {
  if (value.length <= 0) return;
  
  const ideaText = value;
  const ideaHtml = converter.makeHtml(ideaText);
  const brainstormedElem = document.getElementById("brainstormed");

  const newIdeaElem = document.createElement("div");
  newIdeaElem.className += 'idea-card';
  newIdeaElem.innerHTML = ideaHtml;
  brainstormedElem.prepend(newIdeaElem);
};

const onReadyListener = () => {
  /** @type {HTMLTextAreaElement} */
  const ideaElem = document.getElementById("idea");
  const brainstormedElem = document.getElementById("brainstormed");
  let shiftModPressed = false;
  
  if (!ideaElem) return;
  if (!brainstormedElem) return;

  const addNoteBtn = document.getElementById("add-note-btn");
  addNoteBtn.addEventListener("click", () => addNote(ideaElem.value));

  ideaElem.addEventListener("keydown", (e) => {
    if (e.key !== "Shift") return;
    if (e.repeat) return;

    shiftModPressed = true;
  })

  ideaElem.addEventListener("keyup", (e) => {
    if (e.key !== "Shift") return;
    shiftModPressed = false;
  })

  ideaElem.addEventListener("keydown", (e) => {
    if (e.key !== "Enter") return;
    if (!shiftModPressed) return;
    
    addNote(ideaElem.value);
    ideaElem.value = "";
    ideaElem.setAttribute("placeholder", "Tell me more!")

    e.preventDefault();
  })
}

document.addEventListener('DOMContentLoaded', onReadyListener);

Top

Final Result

Finally you should get something like this on your browser.

Third Build

Top

Conclusion

If you’re looking to get started as a JavaScript developer, I would highly recommend looking into webpack or any other build system for that matter. Since you’re already going to encounter them at some point in your journey, they are everywhere. If you’re simply looking for creating a simple vanilla javascript and sass workflow you’re already half way there with the setup we up to this point.

If you haven’t looked at the first article, here it is. There is a repo if you want to take a peek. Thank you for reading.

Top

References


Headshot of Maxi Ferreira

Hi, I'm Zablon. I'm a software engineer, mostly working in the web space, based in Ethiopia. You can follow me on Twitter, see some of my work on GitHub, or read more about Qebero.dev on here.