5

I spent hours on a basic webpack configuration but I'm still not able to make it work. My aim is to perform the parsing of a html template as you import it in a JavaScript file. It looks like a common use-case, but there should be something odd in my webpack configuration or in my understanding.

I looked for configurations of html-loader, html-webpack-plugin, posthtml as well as pug and I've read all of their documentations, but none of them worked.

According to PostHTML Readme:

PostHTML is a tool for transforming HTML/XML with JS plugins. PostHTML itself is very small. It includes only a HTML parser, a HTML node tree API and a node tree stringifier.

So, since it was the most promising, I report my attempt with posthtml:

   rules: [
      {
        test: /.html$/,
        use: [
          {
            loader: "html-loader",
            options: {
              minimize: true,
              interpolation: false
            }
          },
          {
            loader: "posthtml-loader"
          }
        ]
      }
    ]

It doesn't return any error but it looks like is totally ignoring the posthtml-loader, since executing import template from 'src/test.html' I get the template as string (how is supposed to do html-loader alone).

In my understanding, loaders are supposed to compile/transform files with different formats and make them available to JavaScript, and since html is the most common type in a front-end project I supposed it was easy, but I'm not finding anything on the internet related to this question.

What I expect is to have a DOM tree object or, anyway, something that can be used by JavaScript.

Is anyone able to help me?

EDIT: My question is about getting a webpack configuration up and working. I know many solution for parsing HTML strings, but they're not applicable here

3 Answers3

2

Well, transforming a string to html is easy. So if you get a string reposonse back for your template you can transform it to a DOM structure like below.

/** Create a NON attached DOM element. This floats in nothing. Hello Dave */
var dave = document.createElement("div");

/** Give dave a body with the HTML string we received from "somewhere" */
dave.innerHTML = "<div class='foo'><input type='text'></div>";

/**
  * dave is now <div><div class='foo'><input type='text'></div></div>
  */
dave.querySelector('input').value = "I'm sorry Dave, I'm afraid I can't do that";


/** ======================================================================
 * Now render we render Dave, this isn't really needed, but will do anyways.
 * We don't need the "wrapping" floating div we created so we get all of Dave's children
 * And let Dave be forgotten about in the abyss of eternity.
 */
var content = dave.children;

var main = document.getElementById('main');
for(var i = 0; i < content.length; i++) {
  main.appendChild(content[i]);
}
.foo {
   background-color: red;
}
.foo input {
   background-color: black;
   color: white;
}
<body id="main">
</body>

You can then do transformation on the 'children' as well, just as you would with a normal DOM tree object.

Tschallacka
  • 27,901
  • 14
  • 88
  • 133
  • I don't like this solution because I need to parse html in several files, and I can't inject html in the DOM every time just to understand how is the template. I could create a fake document, since javascript allows it, and then use it to parse. Or I could use a separate library. But I'd prefer a webpack-based solution :D it's supposed to work theoretically – Christian Vincenzo Traina May 31 '19 at 09:22
  • @ChristianTraìna You dont have to inject to DOM. only the last line injects to DOM. the rest is purely virtual. You make a virtual unattached element with x. There you insert text. Thats transformed to DOM object. You can do x.getElementById etc... Without the rendered DOM coming in effect – Tschallacka May 31 '19 at 09:25
  • @CristianTraìna I modified the answer with some comments to explain what's happening. – Tschallacka May 31 '19 at 09:42
2

Believe it or not this is not a common use case, importing html files into JavaScript and displaying them sounds like a workaround or a mid-port hack. JavaScript libraries are used to generate dynamic html on the the fly in the browser in response to user input. If this is what you want you should be using React, Vue, Angular, jQuery or similar.

As others have mentioned, the solution to your problem is not to look for a loader that converts HTML into a DOM node, it's for you to do it yourself. The DOM is not present till code is executed in the browser, It's not a compile time transformation. It's a run-time transformation.

import template from 'src/test.html'

const html = document.createElement('div')
html.innerHTML = template

A loader simply cannot do what you ask for. The browser is responsible for parsing HTML and building the DOM tree based on it's platform and vendor specific implementation. querySelector or createElement are API methods for us to access this browser functionality. But we do not own DOM nodes in our code. They are created for us by the platform. Our JavaScript is merely consuming it.

There are some solutions such as DOMParser and jsDOM that can do this on the server. But they are incomplete implementations of the browser DOM and they should not be brought into front-end code meant to be shipped to the browser. They are meant to be used for things such as headless testing, web crawling and other forms of machine consumption of web pages.

If you can present a specific scenario where you cannot have a HTML string parsed in the browser along with the rest of the code and just before it is consumed by JavaScript, by all means show us and we will present a solution. But It is highly likely that you have misunderstood the roles of JavaScript, Webpack, and HTML and the relationship between them.

TLDR: HTML is a string that gets sent to the browser via HTTP and the browser decides how to build a DOM tree based on it's vendor specific implemenation. In front end code there is no alternative except to get the browser to build a DOM tree for you with a string you provide and then consume it with the DOM Api.

Avin Kavish
  • 8,317
  • 1
  • 21
  • 36
  • Thank you for your answer. I thought there was something available to parse HTML like a framework, since Angular, React, Vue and so on, are all based on webpack and I thought they were used all the same way to parse and compose templates. Instead, probably each of them use their own custom parser – Christian Vincenzo Traina Jun 02 '19 at 09:16
  • They use their own compilers to build their own representations of objects, such as `React.createElement('div', childElements)` and so on.... None of the frameworks actually build the DOM till the code is executed in the browser. – Avin Kavish Jun 02 '19 at 09:21
1

To me it seems like the posthtml-loader is primarily a tool that helps to "prepare" your HTML during the build. Its parser options allow you to break in to the string -> PostHTML AST Tree step, and its plugin options allow you to modify the tree. Then, it stringifies back to HTML.

I couldn't find an option in the webpack plugin to return the interim tree format.


You could write a small custom loader to parse HTML strings to DOM objects:

// custom-loader.js
module.exports = function(content) {
  return `module.exports = (function() {
    const parser = new DOMParser();
    const doc = parser.parseFromString("${content}", "text/html");
    return doc.body.children; 
  }())`;
};

Then, in your webpack.config.js, you can tell webpack to make .html files pass this loader:

// webpack.config.js
module.exports = {
  mode: 'development',
  entry: './main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.bundle.js'
  },
  devtool: "eval-source-map",
  module: {
    rules: [
      {
        test: /\.html$/,
        use: [ './custom-loader' ]
      }
    ]
  }
};

Now, whenever you type something like const template = require('./template.html'); you'll get an HTMLCollection instance rather than just a string.


Note that this loader adds a dependency to DOMParser, which is only available in the browser. You could replace it by something like jsdom if you want to run in non-browser environments.

user3297291
  • 22,592
  • 4
  • 29
  • 45
  • You didn't only explain why PostHTML didn't work, but you also provided a working solution. So thank you a lot, this is by far the best answer. Would you advise this solution to implement a model-view-controller structure (with no framework) or you think there are better solutions? – Christian Vincenzo Traina Jun 02 '19 at 09:19
  • I think it's a great idea to import your HTML templates using webpack. I use the same approach when working with knockout.js in an MVVM like fashion. It works well to have (view)models, templates and even styling nicely separated. However, the automatic parsing to DOM elements I could just as well see being part of your client-side code. Like, `new UserProfile( apiUser, Template.fromString(require("./UserProfile.html") ) )`. – user3297291 Jun 03 '19 at 07:54