Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend current functionality to inject partials #1330

Closed
Legends opened this issue Jan 1, 2020 · 13 comments
Closed

Extend current functionality to inject partials #1330

Legends opened this issue Jan 1, 2020 · 13 comments

Comments

@Legends
Copy link

Legends commented Jan 1, 2020

Is your feature request related to a problem? Please describe.
Currently I want to build a static multi-page html app.

As suggested by you, I should provide a template page for example about.html and do:
require("./header")
Here goes my about markup
require("./footer")

I don't like it as I have to repeat header and footer in every page.

Describe the solution you'd like
I currently have a layout.html page which serves as my template for all pages.
This template requires both: header and footer. I don't have to require them in every page now.

Now i only have to change what needs to be different in every page --> the content.

How I would do it:

We define a partials section where we can define a section in the page where this partial needs to be injected (location) and a url where the partials resides (path).
The location can be placeholder/tag/selector.

  new HtmlWebpackPlugin({
            template: './src/pages/layout/layout.html',
            templateParameters: {
                'title': 'Index-Page'              
            },
            partials: [
                {                    
                    path: path.join(__dirname, './src/partials/index/index-body.html'),                
                    location: 'main'  <-- to be specified: can be a tag/placeholder/selector
                },
               ....
            ],
            inject: true,
            chunks: ['index'],
            filename: 'index.html'
        }),

The ChildCompiler.compileTemplate could now create a require statement using the url specified in path and inject the respective require statement in the desired location of the template.

That's it. The further processing is done as usual.
This can be a PR from our side. --> v3.3 ?

Benefits:

  1. And all the partials now can also make use of templateParameters.
  2. And I guess all the partials get watched while developing.
  3. We just require what we really need and don't repeat ourselves.

Describe alternatives you've considered

Alternative 1: Provide a hook before template compilation happens (which would be triggered only if partials are defined) and we could create a plugin, that creates an updated template which includes all the require's and we give you the path of the updated template back . We don't alter the original template but give you a reference to an altered copy.

Alternative 2: We implement a plugin that would include most of the logic you have implemented in html-webpack-plugin. Doesn't feel right, as the logic already exists.
Here we would only need a suggestion from your-side which webpack hook to attach to, in order to make it work on our side.

Additional context

I've installed the beta version for your plugin and realized that my require calls in my layout.html template don't work anymore.

It spits out the require statement in the browser:

   <aside>
            ${require('../../partials/nav.html')}
   </aside>

Is this going to be supported in the next version?

@jantimon
Copy link
Owner

jantimon commented Jan 1, 2020

There are so many different ways to solve this without adding more features to this plugin so I am not sure if the value is high enough to add it to the plugin itself.

Another way could be something like this codesandbox:

It supports:

  • layout wrapper
  • file watching
  • js / css injection

webpack.config.js

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  module: {
    rules: [
      { test: /\.ejs$/, loader: 'ejs-loader' },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "src/layout.js",
      page: 'home',
      filename: 'index.html'
    }),
    new HtmlWebpackPlugin({
      template: "src/layout.js",
      page: 'about',
      filename: 'about.html'
    }),
  ]
};

layout.js

module.exports = templateData => {
  const { htmlWebpackPlugin } = templateData;
  const { page } = htmlWebpackPlugin.options;
  return (
    require("./templates/header.ejs")(templateData) +
    require("./templates/" + page + ".ejs")(templateData) +
    require("./templates/footer.ejs")(templateData)
  );
};

templates/header.ejs

<html>
  <head>
      <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
<body style="background:orange">

templates/footer.ejs

</body>
</html>

templates/home.ejs

<h1>Home</h1>

This is the home page

templates/about.ejs

<h1>About</h1>

This is the about page

What do you think about this example?

@Legends
Copy link
Author

Legends commented Jan 1, 2020

Looks promising! Didn't know I could make it like this.
Yes, I share your concern

" I am not sure if the value is high enough to add it to the plugin itself."

I will give it a try and when everything runs as expected, I will close this request.

Thanks!


But for all who read this thread, if you provide an object for templateParameters,
the name of the page has to be included there.
In layout.ejs change:
const { page } = templateData.htmlWebpackPlugin.options;
to
const { page } = templateData;

new HtmlWebpackPlugin({
     template: "src/layout.js",
     templateParameters: {       
       'title': 'Index',       
       'page': 'home'  // <-- move page name here
     },    
     filename: 'index.html'
   }),

@colbyfayock
Copy link

any idea on how to solve this without requiring maintaining a template? never really liked the idea of having to maintain a template and instead letting it all come together dynamically

i really like the idea of NOT having to do any type of maintaining of the template and can instead focus on a very individual component that needs to be injected like a simple partial that has <div id="root"></div> in it (i keep using the react example)

the template composition there reminds me of creating template components in wordpress 😝 i wonder though if i could utilize some ideas in that approach for the partial plugin i maintain where this issue is stemming from. being able to figure out where to properly hook into html-webpack-plugin to achieve this on my own in html-webpack-partials-plugin would also be very helpful if you dont want to provide any of this kind of support first class

@jantimon
Copy link
Owner

jantimon commented Jan 2, 2020

@colbyfayock the code is quite small

module.exports = templateData => {
  return (
    require("./templates/header.ejs")(templateData) +
    require("./templates/" + templateData.htmlWebpackPlugin.options.page + ".ejs")(templateData) +
    require("./templates/footer.ejs")(templateData)
  );
};

or even with only one wrapper file like in this improved codesandbox

module.exports = templateData => 
  require("./templates/layout.ejs")({...templateData, 
    body: require("./templates/" + templateData.htmlWebpackPlugin.options.page + ".ejs")(templateData)
  });

maintaining only ~5 lines of code which control how your page will be sticked together seems doable to me

This also provides you the flexibility to have exceptions e.g. to have a special header for some pages

But maybe your case is slightly differently (please correct me if I am wrong).
If I understood you correctly you would like to avoid creating an html file like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
     <div id="root"></div>
  </body>
</html>

Here is one possible solution:

You could publish the following code to npm as a new package for example html-webpack-plugin-react-template:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
     <div id="<%= htmlWebpackPlugin.options.rootId || 'root' %>"></div>
  </body>
</html>

Now it can be used inside your project like this:

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: require.resolve('html-webpack-plugin-react-template')
    }),
  ]
};

What do you think about this approach?

@Legends
Copy link
Author

Legends commented Jan 8, 2020

@jantimon I've modified your example.
I have added ${require('./breadcrumbs.ejs')} to the about.ejs file.
breadcrumbs.ejs contains only html and I get Javascript code displayed in about.html.

Just go to the about page in the codesandbox: modified version of your sample.

@jantimon
Copy link
Owner

jantimon commented Jan 8, 2020

The loader does only generate the template function to allow you passing variables.

try for example ${require('./breadcrumbs.ejs')('hello world')} or just ${require('./breadcrumbs.ejs')()}

@Legends Legends closed this as completed Jan 8, 2020
@Legends
Copy link
Author

Legends commented Jan 8, 2020

I will give the last solution a try, thanks for your support!

@Legends
Copy link
Author

Legends commented Jan 8, 2020


@Legends
Copy link
Author

Legends commented Jan 9, 2020

@jantimon ( "html-webpack-plugin": "^4.0.0-beta.11")

Why does the require not resolve the path to my index-body.ejs correctly?
The page resides in ./src/pages/index/index-body.ejs.

Error: Cannot find module './pages/index/index-body.ejs' // <-- should resolve to ../pages
root-template.js:166 webpackContextResolve

pages and templates folder are sitting in the same folder.

./src/templates/root-template.js:

export default templateData => {
    return (
        require("./layout.ejs")({
            ...templateData,
            ...
            body: require(`../pages${templateData.htmlWebpackPlugin.options.body}.ejs`)(templateData),
            ....
        })
    );
};

webpack.config.js:

 new HtmlWebpackPlugin({
            inject: true,
            chunks: ['index'],
            filename: 'index.html',
            template: './src/templates/root-template.js',
            body: '/index/index-body',
            title: 'Index-Page'        
        }),

@Legends Legends reopened this Jan 9, 2020
@jantimon
Copy link
Owner

jantimon commented Jan 13, 2020

You have to use a slash after pages so webpack can guess the folder correctly:

require(`../pages/

Require calls are always relative to the current file.
But this is unrelated to the html-webpack-plugin.
This is how webpack require works.

@Legends Legends closed this as completed Jan 13, 2020
@Legends
Copy link
Author

Legends commented Jan 28, 2020

@jantimon

One issue I've just experienced right now with the above approach, you may not put your entry scripts side-by-side with your page templates!

For example I have the following structure:
(pages folder is where I keep my body-templates for each page)
image

Now I decided to put the entry scripts (under folder entry) into their respective pages folder in order to have them side-by-side:
image

But this won't work, because of the dynamic require I guess and will result in:
Error: Callback was already called.

webpack --config webpack.config.dev.js --verbose --display-error-details --colors --mode development
C:\Users\xxx\Source\Repos\legends.static.github.io\node_modules\neo-async\async.js:16
throw new Error('Callback was already called.');
^

Error: Callback was already called.
at throwError (C:\Users\xxx\Source\Repos\legends.static.github.io\node_modules\neo-async\async.js:16:11)
at C:\Users\xxx\Source\Repos\legends.static.github.io\node_modules\neo-async\async.js:2818:7
at processTicksAndRejections (internal/process/task_queues.js:76:11)
error Command failed with exit code 1.

I have changed the file location of my entry scripts and went on to do something else and later wondered why it doesn't work anymore. :-)

@jantimon
Copy link
Owner

Sorry I am really not sure why this is happening :(

@Legends
Copy link
Author

Legends commented Jan 28, 2020

Yeah, I know, if I find time I will create a little repo for this, but probably this is expected when using dynamic require calls.
I'll create one these days or next week.

@lock lock bot locked as resolved and limited conversation to collaborators Feb 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants