Alex Newman
Alex Newman

Reputation: 11

Creating multiple HTML files from a single Jade template with Grunt

I'm looking to create multiple HTML files from a single Jade template using Grunt.

Here's what I'm doing:

  1. Grabbing the JSON data from an external file
  2. Looping through that object
  3. Creating a grunt config task for each value in that JSON object

Here's my code:

neighborhoods = grunt.file.readJSON('data/neighborhoods.json');

for(var i = 0; i < Object.keys(neighborhoods).length; i++) {

    var neighborhood = {
        "title" : Object.keys(neighborhoods)[i],
        "data"  : neighborhoods[Object.keys(neighborhoods)[i]]
    };

    grunt.config(['jade', neighborhood.title], {
        options: {
            data: function() {
                return {
                    neighborhoods: neighborhood.data
                }
            }
        },
        files: {
            "build/neighborhoods/<%= neighborhood.title %>.html": "layouts/neighborhood.jade"
        }
    });
}

The problem that I'm running in to is this

Running "jade:Art Museum" (jade) task
Warning: An error occurred while processing a template (Cannot read property 'title' of undefined). Use --force to continue.

If I make the filename a string, it runs fine but obviously creates all the files with the same filename, thus only creating one file. I need to make that filename dynamic.

Upvotes: 0

Views: 1806

Answers (3)

Wayne Shelley
Wayne Shelley

Reputation: 1032

I know this is an old post but I kept coming back here whilst trying to solve a similar problem. I wanted to output multiple html files from a single jade template file using a for-loop.

The two problems I came across was setting the output filename (a javascript object literal KEY) and making sure inline javascript functions are run immediately so that the loop variables are available.

Here is my full source code with comments. I hope this is of use to anyone else stumbling across this post.

Gruntfile.js:

module.exports = function(grunt) {

  // Create basic grunt config (e.g. watch files)
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    watch: {
      grunt: { files: ['Gruntfile.js'] },
      jade: {
        files: 'src/*.jade',
        tasks: ['jade']
      }
    }
  });

  // Load json to populate jade templates and build loop
  var json = grunt.file.readJSON('test.json');

  for(var i = 0; i < json.length; i++) {
      var obj = json[i];

      // For each json item create a new jade task with a custom 'target' name.
      // Because a custom target is provided don't nest options/data/file parameters 
      // in another target like 'compile' as grunt wont't be able to find them 
      // Make sure that functions are called using immediate invocation or the variables will be lost
      // http://stackoverflow.com/questions/939386/immediate-function-invocation-syntax      
      grunt.config(['jade', obj.filename], {
        options: {
            // Pass data to the jade template
            data: (function(dest, src) {
                return {
                  myJadeName: obj.myname,
                  from: src,
                  to: dest
                };
            }()) // <-- n.b. using() for immediate invocation
        },
        // Add files using custom function
        files: (function() {
          var files = {};
          files['build/' + obj.filename + '.html'] = 'src/index.jade';
          return files;
        }()) // <-- n.b. using () for immediate invocation
      });
  }

  grunt.loadNpmTasks('grunt-contrib-jade');
  grunt.loadNpmTasks('grunt-contrib-watch');

  // Register all the jade tasks using top level 'jade' task
  // You can also run subtasks using the target name e.g. 'jade:cats'
  grunt.registerTask('default', ['jade', 'watch']);
};

src/index.jade:

doctype html
html(lang="en")
  head
    title= pageTitle
    script(type='text/javascript').
      if (foo) {
         bar(1 + 5)
      }
  body
    h1 #{myJadeName} - node template engine    
    #container.col
      p.
        Jade is a terse and simple
        templating language with a
        strong focus on performance
        and powerful features.

test.json:

[{
    "id" : "1", 
    "filename"   : "cats",
    "tid" : "2016-01-01 23:35",
    "myname": "Cat Lady"
},
{
    "id" : "2", 
    "filename"   : "dogs",
    "tid" : "2016-01-01 23:45",
    "myname": "Dog Man"
}]

After running 'grunt' the output should be:

build/cats.html
build/dogs.html

Upvotes: 1

Georgy Malanichev
Georgy Malanichev

Reputation: 336

Came across a similar requirement for a project I'm working on but couldn't get this to work. I kept getting only one file generated since the grunt option had same value for all tasks (the last value). So I ended up using <%= grunt.task.current.target %> for the file name which in your case would be same as neighborhood.title.

Upvotes: 0

Alex Newman
Alex Newman

Reputation: 11

I found the solution here:

Use Global Variable to Set Build Output Path in Grunt

The issue is that the module exports before those global variables get set, so they are all undefined in subsequent tasks defined within the initConfig() task.


This did the trick!

var neighborhoods = grunt.file.readJSON('data/neighborhoods.json');

for(var i = 0; i < Object.keys(neighborhoods).length; i++) {

    var neighborhood = {
        "title" : Object.keys(neighborhoods)[i],
        "data"  : neighborhoods[Object.keys(neighborhoods)[i]]
    };

    /* 
     * DEFINE VALUE AS GRUNT OPTION
     */

    grunt.option('neighborhood_title', neighborhood.title);

    grunt.config(['jade', neighborhood.title], {
        options: {
            data: function() {
                return {
                    neighborhoods: neighborhood.data,
                    neighborhood_title: neighborhood.title
                }
            }
        },

        /* 
         * OUTPUT GRUNT OPTION AS FILENAME
         */

        files: {
            "build/neighborhoods/<%= grunt.option('neighborhood_title') %>.html": "layouts/neighborhood.jade"
        }
    });
}


This results in the desired output:

Running "jade:East Passyunk" (jade) task
File build/neighborhoods/Society Hill.html created.

Running "jade:Fishtown" (jade) task
File build/neighborhoods/Society Hill.html created.

Running "jade:Graduate Hospital" (jade) task
File build/neighborhoods/Society Hill.html created.

Running "jade:Midtown Village" (jade) task
File build/neighborhoods/Society Hill.html created.

Running "jade:Northern Liberties" (jade) task
File build/neighborhoods/Society Hill.html created.

...

Upvotes: 1

Related Questions