patrickkettner/grunt-compile-handlebars

Help with helper syntax

dotherightthing opened this issue · 50 comments

I'm using the latest version of grunt-compile-handlebars (^2.0.0).

I've been using the following helper pattern successfully with kss-node:

module.exports = function(handlebars) {
    handlebars.registerHelper('zeroOffset', function(_index) {
        return new handlebars.SafeString( parseInt(_index, 10) + 1 );
    });
};

grunt-compile-handlebars is loading my helper script via the helpers option. I know this is working because if I change the filename I get an error:

Warning: ENOENT, no such file or directory

I use the helper like this:

{{#each myLoop}}
<p>{{zeroOffset @index}}</p>
{{/each}}

However when I use it with grunt-compile-handlebars. I get:

Warning: Missing helper: "zeroOffset" Use --force to continue.

If I change the syntax to match the patterns used on handlebarsjs.com, eg:

Handlebars.registerHelper('zeroOffset', function(context, options) {..}

I get:

Warning: Handlebars is not defined Use --force to continue.

I've checked out your test super_helper.js but it doesn't use a name, so I can't call it the way I want to.

Any pointers you could give me would be really helpful thanks.

Cheers,
Dan

hey dan!
the name of each helper is based off of its file name. The super_helper you noticed in our repo is available as {{> super helper}}.

So you are actually really close in your example. You would just need to change this

module.exports = function(handlebars) {
    handlebars.registerHelper('zeroOffset', function(_index) {
        return new handlebars.SafeString( parseInt(_index, 10) + 1 );
    });
};

to this

module.exports = function(_index) {
    return new handlebars.SafeString( parseInt(_index, 10) + 1 );
};

and name that file zeroOffest

Hi Patrick,

Thanks for your super-fast reply!

I changed the syntax as suggested but the helper still isn't being recognised:

    // zeroOffset.js
    module.exports = function(_index) {
        return new handlebars.SafeString( parseInt(_index, 10) + 1 );
    };

    // Gruntfile.js
    'compile-handlebars': {
      modules: {
        files: [{
            expand: true,
            cwd: '<%= project.dev.modules %>/',
            src: [
              '**/*.hbs',
              '!**/_*.hbs',
              '!**/OLD_*.hbs' // prototypes
            ],
            dest: '<%= project.build.html_compiled_includes %>/',
            ext: '.html'
        }],
        partials: ['<%= project.dev.modules %>/**/_*.hbs'],
        helpers: '<%= project.dev.helpers %>/handlebars/zeroOffset.js',
        registerFullPath: true,
        templateData: '<%= project.dev.modules %>/**/*.json'
    }

Is there anything else that is obviously amiss here?

Thanks,
Dan

use an array for helpers, rather just a string

Thanks, but same deal with an array:

helpers: ['<%= project.dev.helpers %>/handlebars/zeroOffset.js'],

hmm... whats the exact error you are getting now?

Same error:

Warning: Missing helper: "zeroOffset" Use --force to continue.

Would you be able to place some logging here to get the path to the helpers being registered?

  console.log( name ); // resources/dev/helpers/handlebars/zeroOffset
  console.log( helper ); // resources/dev/helpers/handlebars/zeroOffset.js
  console.log( fs.realpathSync(helper) ); // /Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js

is /Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js the correct file?

Yes, here is the full file:

nano /Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js

/** @method
 * @name zeroOffset
 *
 * @author dan.smith@chrometoaster.com
 *
 * @summary Used with each loops to offset the zero based numbering (i.e. 0 -> 1)
 *
 * @param {Number} index - The loop index
 */

// kss-node syntax:
/*
    module.exports = function(handlebars) {
        handlebars.registerHelper('zeroOffset', function(_index) {
            return new handlebars.SafeString( parseInt(_index, 10) + 1 );
        });
    };
*/

// grunt-compile-handlebars syntax

    module.exports = function(_index) {
        return new handlebars.SafeString( parseInt(_index, 10) + 1 );
    };

It looks to me like compile-handlebars.js is receiving a path where it expects a name.

Could this be a side effect of the registerFullPath option?

are you exposing two modules through module.export? If that is the case, thats invalid. Try requireing the file in a node repl and you should see it results in just an empty object being returned

Sorry can you repeat in laymans terms? What's a node repl? I'm only including this helper once, via the helpers option. It's not used with kss-node, only with grunt-compile-handlebars.

Also FYI, if I do the following I get the handlebars is not defined error:

  var name_unpathed = name.substr( name.lastIndexOf('/') + 1 );

  handlebars.registerHelper(name_unpathed, require(fs.realpathSync(helper)));

  console.log( name_unpathed );
  console.log( helper );
  console.log( fs.realpathSync(helper) );

sorry, didn't mean to use buzzwords :[

the repl (or Read-Eval-Print-Loop) is just the program that opens up when you run node by itself in the command line. This is really similar to the console in a browser, in that you type in commands and they get immediately executed and returned.

What I was suggesting is opening up node and then running var zeroOffset = require('/Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js')

then checking to see what zeroOffset is. If the module is valid, you will get a function. If its invalid it will be {} or undefined

you are getting the handlebars is not defined error because handlebars is not defined :]

you will need to require handlebars in your helper in order to have access to it. Its not a global

Ok thanks for clarifying.

I'm not sure if I should require the bin executable or the JS file -

Attempt 1:

var handlebars = require('../../../../node_modules/grunt-compile-handlebars/node_modules/handlebars/dist/handlebars.js');

module.exports = function(_index) {
    return new handlebars.SafeString( parseInt(_index, 10) + 1 );
};

Output 1:

undefined

Attempt 2:

var handlebars = require('../../../../node_modules/grunt-compile-handlebars/node_modules/handlebars/bin/handlebars');

module.exports = function(_index) {
    return new handlebars.SafeString( parseInt(_index, 10) + 1 );
};

Output 2:

var zeroOffset = require('/Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js')
Precompile handlebar templates.
Usage: node [template|directory]...
Options:
-f, --output Output File
--map Source Map File [string] [default: undefined]
-a, --amd Exports amd style (require.js)
-c, --commonjs Exports CommonJS style, path to Handlebars module [default: null]
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [default: ""]
-k, --known Known helpers
-o, --knownOnly Known helpers only
-m, --min Minimize output
-n, --namespace Template namespace [default: "Handlebars.templates"]
-s, --simple Output template function only.
-r, --root Template root. Base value that will be stripped from template names.
-p, --partial Compiling a partial template
-d, --data Include data when compiling
-e, --extension Template extension. [default: "handlebars"]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates.
-v, --version Prints the current compiler version
--help Outputs this message

from within zeroOffset.js, add var handlebars = require('handlebars')

Yes, so I tried that first (sorry didn't mention it), and I got undefined

that would mean you haven't installed it.

npm install handlebars --save

But why can't I use your copy?

Ok,

npm install handlebars --save

node

var zeroOffset = require('/Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js')

Returns

undefined

you could, but its in a different module scope. You would need to use a relative path from that file to the node_modules/grunt-compile-handlebars/node_modules/handlebars

bits are pretty much free, so if it was me, I would just add handlebars as a dep than have ugly code.

yeah, any variable assigned will return undefined. What is zeroOffset equal to?

zeroOffset = the function in the zeroOffset.js file?

var handlebars = require('handlebars');

module.exports = function(_index) {
    return new handlebars.SafeString( parseInt(_index, 10) + 1 );
};

So if no _index argument is passed in I suppose it will be undefined?

so its working?

I am not sure - thats a handlebars question more than anything

Ah no, after all this I still get

Warning: Missing helper: "zeroOffset" Use --force to continue.

However if I implement my earlier hotfix into compile-handlebars.js, it does work:

  var name_unpathed = name.substr( name.lastIndexOf('/') + 1 );

  handlebars.registerHelper(name_unpathed, require(fs.realpathSync(helper)));

im confused as to what the issue is because we have gone back and forth so much.

could you resumarize?

Sure, to summarise:

I have a Grunt compile-handlebars:modules task which I run on .hbs files:

'compile-handlebars': {
  modules: {
    files: [{
        expand: true,
        cwd: '<%= project.dev.modules %>/',
        src: [
          '**/*.hbs',
          '!**/_*.hbs',
          '!**/OLD_*.hbs' // prototypes
        ],
        dest: '<%= project.build.html_compiled_includes %>/',
        ext: '.html'
    }],
    partials: ['<%= project.dev.modules %>/**/_*.hbs'],
    helpers: ['<%= project.dev.helpers %>/handlebars/zeroOffset.js'],
    registerFullPath: true,
    templateData: '<%= project.dev.modules %>/**/*.json'
  }
},

The zeroOffset.js helper is used like so in .hbs files, to output 1,2,3.. rather than 0,1,2...:

{{#each someLoop}}
{{zeroOffset @index}}
{{/each}}

zeroOffset.js contains the following:

var handlebars = require('handlebars'); // we installed and referenced this to fix an error

module.exports = function(_index) {
    return new handlebars.SafeString( parseInt(_index, 10) + 1 );
};

When I run grunt compile-handlebars:modules I get:

Warning: Missing helper: "zeroOffset" Use --force to continue.

You suggested adding logging to compile-handlebars.js.

I noted that the name passed to handlebars.registerHelper was actually the relative path to the helper file:

handlebars.registerHelper(name, require(fs.realpathSync(helper))); // line 203
console.log( name ); // resources/dev/helpers/handlebars/zeroOffset
console.log( helper ); // resources/dev/helpers/handlebars/zeroOffset.js
console.log( fs.realpathSync(helper) ); // /Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js

I stripped the path off the name and it now works:

var name_unpathed = name.substr( name.lastIndexOf('/') + 1 );
handlebars.registerHelper(name_unpathed, require(fs.realpathSync(helper))); // line 203
console.log( name ); // zeroOffset
console.log( helper ); // resources/dev/helpers/handlebars/zeroOffset.js
console.log( fs.realpathSync(helper) ); // /Users/Dan/Websites/outwardbound-2015/wp-content/themes/outwardbound2014/resources/dev/helpers/handlebars/zeroOffset.js

@patrickkettner Hi, could you please publish this hotfix to npm? I could really use it, thanks.

So what should be a helper syntax ?
I'm facing this too: Warning: Missing helper: "..." Use --force to continue.

@zuzana what hotfix?

@aakrem export a function that has the signature of a handlers helper. e.g. this

@patrickkettner I just got confused with the dates that were referenced in the discussion above, so I thought that the hotfix from @dotherightthing was not published yet. Then I got all I needed to work as I wanted and forgot to comment on that.

TL;DR: All is well, thanks for this grunt plugin!

@patrickkettner
Gruntfile.js

'compile-handlebars': {
  globalJsonGlobbedTemplate: {
    files: [{
      expand: true,
        cwd: './fixtures/',
        src: './*.handlebars',
        dest: './',
        ext: '.html'
      }],
      templateData: './fixtures/data.json',
      helpers: './fixtures/helpers/*.js',
    }
  }
}

./fixtures/helpers/helper.js

var Handlebars = require("handlebars");
module.exports = function() {
    return new Handlebars.registerHelper("checkStatus", function (status) {
    });
};

./fixtures/template.handlebars

<div>{{checkStatus}}</div>

Warning: Missing helper: "checkStatus" Use --force to continue.

@aakrem your helper file should have the same name as your helper.

./fixtures/helpers/checkStatus.js instead of ./fixtures/helpers/helper.js

thanks

Need help with handlebars helper.

Helper

var Handlebars = require('handlebars');

module.exports = function() {
    Handlebars.registerHelper("ifCond", function (v1, operator, v2, options) {
        switch (operator) {
            case '==':
                return (v1 == v2) ? options.fn(this) : options.inverse(this);
            case '===':
                return (v1 === v2) ? options.fn(this) : options.inverse(this);
            case '<':
                return (v1 < v2) ? options.fn(this) : options.inverse(this);
            case '<=':
                return (v1 <= v2) ? options.fn(this) : options.inverse(this);
            case '>':
                return (v1 > v2) ? options.fn(this) : options.inverse(this);
            case '>=':
                return (v1 >= v2) ? options.fn(this) : options.inverse(this);
            case '&&':
                return (v1 && v2) ? options.fn(this) : options.inverse(this);
            case '||':
                return (v1 || v2) ? options.fn(this) : options.inverse(this);
            default:
                return options.inverse(this);
        }
    });
}

Gruntfile.js

"compile-handlebars": {
    tpl: {
        files: [{
            src: "<%= path.tpl %>/tpl.hbs",
           dest: "<%= path.root %>/<%= path.build.tpl %>/build.html"
        }],
        templateData: "<%= path.json %>/test.json",
        helpers: "grunt/handlebars-helpers/ifCond.js"
    }
}

Template

{{#test}}
    {{#ifCond @index '==' 4}}
        <div>{{@index}}</div>
    {{/ifCond}}
{{/test}}

This code do nothing. What i do wrong?

what is the #test helper?

#test is json object:

{
  "test": {
    "el0": "...",
    "el1": "...",
    ...
  },
  ...
}

if skip custom helper this code compiled fine:

{{#test}}
    <div>{{@index}}</div>
{{/test}}

The issue with your helper is that you appear to be trying to access the index from outside of a loop

you want something like this

{{#each array}}
  {{#ifCond @index '==' 4}}
    <div>{{@index}}</div>
  {{/ifCond}}
{{/each}}
{ 
  array: ['a', 'b', 'c', 'd', 'e', 'f']
}

with the helper you already supplied

I get <div>4</div> as output

try it out at tryhandlebars if you are trying to troubleshoot

Hi, I've installed the node module but I don't see the hotfix and I am getting the "helper not found' message. Any thoughts?

@dcilibiu there has never been a hotfix. @dotherightthing pushed a change to his own branch and there hasn't been a PR for it

I'm happy to help, but I would need a lot more information. How is your project setup? what are the folders and file structure, what does your helper look like, etc

For now it's good, I've added the helper in the root of the proj and it is working. Now it is not searching for the path only for the file (helpers: ['equal.js'])

I too can't get this to work.

Gruntfile.js

"compile-handlebars": {
            static: {
                files: [{
                    expand: true,
                    cwd: 'src/views/pages/',
                    src: '**/*.hbs',
                    dest: 'dist/',
                    ext: '.html'
                }],
                partials: 'src/views/partials/*.hbs',
                helpers: ['src/views/helpers/inc.js']
            }
        }

src/views/helpers/inc.js

module.exports = function() {
    return new Handlebars.registerHelper("checkStatus", function (value) {
        return parseInt(value) + 1;
    });
};

view.hbs

<h2 class="heading-small participant__header">
    {{inc @index}}.
    {{name}}
</h2>

Error: Uncaught Error: Missing helper: "inc"

@olefrank the name of the helper comes from the registerHelper argument, not the filename. So you will want to be using checkStatus

Thanks Patrick. Now I don't get the "missing helper" error :)

But the helper is still not working. I tried several variations but nothing works.

I need a helper that adds 1 to the index (zero based). Like this:

  1. item x
  2. item y
  3. item z

This is what I tried so far. Nothing works. What am I missing??

module.exports = function() {
    return new Handlebars.registerHelper("inc", function (value) {
        return parseInt(value, 10) + 1;
    });
};
module.exports = function(value) {
    return new parseInt(value, 10) + 1 );
};
Handlebars.registerHelper("inc", function (value) {
    return parseInt(value, 10) + 1;
});

@olefrank can you post the full code somewhere? The config, the package.json, the helper, everything. happy to debug, but I need to see a larger picture