Why can't I call my Handlebars partial inside #each? - javascript

While refactoring some code I got this problem: When applying a Handlebars template that uses a partial, it complains about You must pass a string or Handlebars AST to Handlebars.compile. You passed function .... The function is this:
function (context, options) {
options = options || {};
var namespace = options.partial ? options : env,
helpers,
partials;
if (!options.partial) {
helpers = options.helpers;
partials = options.partials;
}
var result = templateSpec.call(
container,
namespace, context,
helpers,
partials,
options.data);
if (!options.partial) {
checkRevision(container.compilerInfo);
}
return result;
}
What I did:
A partial is used to iterate over a list (called members) to build a ul of checkboxes. I found out I needed to make a list of lists and put the call to the partial inside an #each and updated the template input.
I have this:
Template:
...
{{#each hierarchy.levels}}
<ul>
{{> mypartial}}
</ul>
{{/each}}
Partial (simplified):
{{#each members}}
<li>{{this.id}}</li>
{{/each}}
I've checked that each hierarchy.levels in my data structure has a members list.
If I replace the #each in my template with #with hierarchy.levels.[0] (for instance) it works but it won't work when iterating over levels.

I solved it. One of the 2nd level lists were empty (ie zero items) which led to Handlebars rendering it as an empty string which again led to Handlebars thinking that something was false and trying to render the partial as a regular template.

Related

Passing an Array to Handlebars Custom Helper

I am trying to pass an array of data to a custom helper with express-handlebars. I can't get it to work.
I can pass in a string and I can pass in an array to an individual view, but I can't seem to pass in an array to a custom helper. Here is my code:
String Passed to Custom Helper (this works)
// app.js
const hbs = expressHandlebars.create({
extname: 'html',
helpers: {
foo: function () {
return 'FOO!'
}
})
// views/layout/main.html
<h2>{{ foo }}</h2>
// Result --> FOO! outputed as expected
Passing an Array to an Individual view (this also works)
// controllers/pageControllers.js
exports.getIndexPage = (req, res) => {
res.render('index', {
testList: ['one', 'two', 'three']
})
}
// views/index.html
{{#each testList}}
<div>{{this}}</div>
{{/each}}
// Result --> one, two, three outputted as expected
Array Passed to a Custom Helper (this does NOT work)
// app.js
const hbs = expressHandlebars.create({
extname: 'html',
helpers: {
testList2: function () {
return ['one', 'two', 'three']
},
}
})
// views/layout/main.html
{{#each testList2}}
<div>{{this}}</div>
{{/each}}
// Result --> NOTHING. No output, no error messages.
Do custom helpers not take arrays? The same code works on an individual view. The same code works for a string on a custom helper. It just does not work for an array on a custom helper.
If custom helpers do NOT accept arrays, then how can I get an array of data into my main layout file? If it does take an array, then what I am doing wrong and how to get this to work?
Thanks.
According to documentation#helpers, -i'm not using express-handlebars myself.
A Handlebars helper call is a simple identifier, followed by zero or more parameters (separated by a space)
This means you call it with the first identifier and what follows is to be evaluated.
The helper just takes in data and outputs a manipulated outcome to inject in the view.
Taking in data would look something like {{ foo exp1 exp2 }} which translates to {{foo(exp1,exp2)}} and the helper would look something like this.
foo: function (exp1, exp2) {
return `this is ${exp1} and ${exp2}`
}
It's like invoking a regular function.
In your first example you call foo() (the helper function) which takes nothing in but outputs a string, and when activated you get the value return.
In your second example you are using express to render a variable which is loopable on a view. nothing to do with handlebars instance creating. (you are creating basicly an express res.locals.testList variable.)
In your third example you try to loop the helper function and not really invoking it. It's trying to do something like this:
for(let v of function() {}) {
//...
}
This will give the error (probably silent in your case,) is not iterable
It's seems like you should manipulate the data array inside the helper function and then return a string or an html string (using Handlebars.Safestring as mentioned in the docs for escaping html). Then in your view just call it {{ testList2 }} which will out put a ready made HTML for the structure you were trying to do.
Hope this makes sense.
That been said, There are more advanced ways of using helpers as mentioned in docs. that maybe could fit more for whatever it is you are trying to do.

Assign value to a locals variable in keystonejs

In keystonejs (a node cms), I am using handlebars as templating engine in keystonejs. In handlebar file, I am using #each to iterate through an array.
{{#each data.positions}}
<h1 onclick = "callFunction()"> {{title}} </h1>
{{/each}}
Here I want to call a function within route, How can I do it?
Other way that is coming in my mind to to initialize locals variable. Like I have a variable locals.selectedPositionIndex in route file and I want to assign #index value of specific h1 element to this variable when any h1 element is clicked.
You could include a script at the bottom of your Handlebars template and include the function there:
<script>
document.querySelector('h1').addEventListener('click', function() {
// Code here
})
</script>
Alternatively, there is a question here with a useful answer:
Passing a function into a Handlebars template
I have amended the answer from the above question to work with KeystoneJS:
In your view you can assign your function to locals:
locals.func = function () {
// Code here
};
Then you will need to create a Handlebars helper in /templates/views/helpers/index.js:
_helpers.stringifyFunc = function(fn) {
return new hbs.SafeString("(" + fn.toString().replace(/\"/g,"'") + ")()");
};
You can then use this in your Handlebars template:
<h1 onclick="{{ stringifyFunc func }}">{{ title }}</h1>
I hope this helps.

Something like "select" from "where" with Handlebars.js

Using handlebars.js as a template engine I need to query a json file and from here loop through an array within an object in said file. The flow is intended to work like: If a value define via data-attribute on click matches a string within an object array then loop and show data based on those conditions.
Essentially I need a query it like I would back-end to a database,(Using Laravel) e.g:
$attr = "Red";
$prodcuts = DB::table('products')->Where('tags', $tag)->get();
I am working from a very large json file for this. For example sake I created a much smaller one if you need it reference sake:
https://gist.github.com/Panoply/15dc30a9fc598d07b24f0f13a5d42df4
I am aware that with handlebars.js I need to create a helper to make a string with a value:
Handlebars.registerHelper("ifValue", function(conditional, options) {
if (conditional !== options.hash.equals) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
Then I would have my handlebars loop:
<ul>
{{#each products}}
{{#ifvalue tags equals='Blue'}}
<li>{{title}}</li>
<li>{{handle}}</li>
<li>{{tags}}</li>
{{#with images.[0]}}<li>{{src}}</li>{{/with}}
{{#with variants.[0]}}<li>{{price}}</li>{{/with}}
{{/ifvalue}}
{{/each}}
</ul>
Issue is I get a blank result. You can try these code block at http://tryhandlebarsjs.com/ – Considering that the tags are in an object array I might be having some conflict.
Figured it out. Handlebars is touchy. If you stumble across this, here is how I fixed it:
Handlebars:
{{#products}}
{{#inArray tags "Red"}}
{{title}}
{{#with images.[0]}}<img src="{{ this.src }}">{{/with}}
{{/inArray}}
{{/products}}
Helper (Create an inArray):
Handlebars.registerHelper('inArray', function(array, value, options) {
if (array.indexOf.call(array, value) >= 0) {
return options.fn(this);
} else {
return options.inverse(this);
};
});

Emberjs: How to render a view with the view name in variable?

i would like to add some cards to a board, all cards are very different. so i want to create different view for each card that can bind different events and template. I set a 'type' property in the card model which to distinguish the cards.
the board template look like below:
{{#each card in cards}}
{{render card.type card class="card"}}
{{/each}}
However, the first argument for the render help can not be a variable, it can only be the card view name string.
anyone know how to achieve this?
As you already mentioned the built-in render helper only accepts a string to lookup the template to render, therefore one possible solution would be to write your own custom render helper, which then after getting the correct name with card.get('type') delegates the rendering to the built-in render helper. This could look something like this:
Ember.Handlebars.registerBoundHelper('renderCard', function(context, card, options) {
return Ember.Handlebars.helpers.render.call(context, card.get('type'), 'card', options);
});
After that, you could use it like this in your template:
{{#each card in cards}}
{{renderCard this card}}
{{/each}}
Edit
I've also added a basic jsbin that shows it working.
Edit 2
Edited the jsbin again, using object data to be rendered into the template, see here.
Edit 3
Lamentably the DS.FixtureAdapter does not support embedded records which is what you need to make it work. But you could configure you orignal DS.RESTAdapter like this:
App.Adapter = DS.RESTAdapter.extend();
App.Adapter.map('App.Dashboard',
cards: {embedded: 'always'}
);
App.Store = DS.Store.extend({
adapter: App.Adapter
});
This way the card records are always loaded with the parent record. I guess doing this change would make the call to card.get('type') in you handlebar helper return the value rather then undefined.
Hope it helps.
For Ember 1.9 solution above doesn't work. Use another way to register helper:
Ember.Handlebars.registerHelper 'renderDynamic', (context, model, options)->
contextString = options.hash.contextString
model = options.data.view.getStream(model).value()
return Ember.Handlebars.helpers.render(model.get('type'), contextString, options)
And in the template:
= hb 'renderDynamic "this" currentModel contextString="curremtModel"
or in handlebars:
{{renderDynamic "this" currentModel contextString="curremtModel"}}

Passing variables through handlebars partial

I'm currently dealing with handlebars.js in an express.js application. To keep things modular, I split all my templates in partials.
My problem: I couldn't find a way to pass variables through an partial invocation. Let's say I have a partial which looks like this:
<div id=myPartial>
<h1>Headline<h1>
<p>Lorem ipsum</p>
</div>
Let's assume I registered this partial with the name 'myPartial'. In another template I can then say something like:
<section>
{{> myPartial}}
</section>
This works fine, the partial will be rendered as expected and I'm a happy developer. But what I now need, is a way to pass different variables throught this invocation, to check within a partial for example, if a headline is given or not. Something like:
<div id=myPartial>
{{#if headline}}
<h1>{{headline}}</h1>
{{/if}}
<p>Lorem Ipsum</p>
</div>
And the invokation should look something like this:
<section>
{{> myPartial|'headline':'Headline'}}
</section>
or so.
I know, that I'm able to define all the data I need, before I render a template. But I need a way to do it like just explained. Is there a possible way?
Handlebars partials take a second parameter which becomes the context for the partial:
{{> person this}}
In versions v2.0.0 alpha and later, you can also pass a hash of named parameters:
{{> person headline='Headline'}}
You can see the tests for these scenarios: https://github.com/wycats/handlebars.js/blob/ce74c36118ffed1779889d97e6a2a1028ae61510/spec/qunit_spec.js#L456-L462
https://github.com/wycats/handlebars.js/blob/e290ec24f131f89ddf2c6aeb707a4884d41c3c6d/spec/partials.js#L26-L32
Just in case, here is what I did to get partial arguments, kind of. I’ve created a little helper that takes a partial name and a hash of parameters that will be passed to the partial:
Handlebars.registerHelper('render', function(partialId, options) {
var selector = 'script[type="text/x-handlebars-template"]#' + partialId,
source = $(selector).html(),
html = Handlebars.compile(source)(options.hash);
return new Handlebars.SafeString(html);
});
The key thing here is that Handlebars helpers accept a Ruby-like hash of arguments. In the helper code they come as part of the function’s last argument—options— in its hash member. This way you can receive the first argument—the partial name—and get the data after that.
Then, you probably want to return a Handlebars.SafeString from the helper or use “triple‑stash”—{{{— to prevent it from double escaping.
Here is a more or less complete usage scenario:
<script id="text-field" type="text/x-handlebars-template">
<label for="{{id}}">{{label}}</label>
<input type="text" id="{{id}}"/>
</script>
<script id="checkbox-field" type="text/x-handlebars-template">
<label for="{{id}}">{{label}}</label>
<input type="checkbox" id="{{id}}"/>
</script>
<script id="form-template" type="text/x-handlebars-template">
<form>
<h1>{{title}}</h1>
{{ render 'text-field' label="First name" id="author-first-name" }}
{{ render 'text-field' label="Last name" id="author-last-name" }}
{{ render 'text-field' label="Email" id="author-email" }}
{{ render 'checkbox-field' label="Private?" id="private-question" }}
</form>
</script>
Hope this helps …someone. :)
This can also be done in later versions of handlebars using the key=value notation:
{{> mypartial foo='bar' }}
Allowing you to pass specific values to your partial context.
Reference: Context different for partial #182
This is very possible if you write your own helper. We are using a custom $ helper to accomplish this type of interaction (and more):
/*///////////////////////
Adds support for passing arguments to partials. Arguments are merged with
the context for rendering only (non destructive). Use `:token` syntax to
replace parts of the template path. Tokens are replace in order.
USAGE: {{$ 'path.to.partial' context=newContext foo='bar' }}
USAGE: {{$ 'path.:1.:2' replaceOne replaceTwo foo='bar' }}
///////////////////////////////*/
Handlebars.registerHelper('$', function(partial) {
var values, opts, done, value, context;
if (!partial) {
console.error('No partial name given.');
}
values = Array.prototype.slice.call(arguments, 1);
opts = values.pop();
while (!done) {
value = values.pop();
if (value) {
partial = partial.replace(/:[^\.]+/, value);
}
else {
done = true;
}
}
partial = Handlebars.partials[partial];
if (!partial) {
return '';
}
context = _.extend({}, opts.context||this, _.omit(opts, 'context', 'fn', 'inverse'));
return new Handlebars.SafeString( partial(context) );
});
Sounds like you want to do something like this:
{{> person {another: 'attribute'} }}
Yehuda already gave you a way of doing that:
{{> person this}}
But to clarify:
To give your partial its own data, just give it its own model inside the existing model, like so:
{{> person this.childContext}}
In other words, if this is the model you're giving to your template:
var model = {
some : 'attribute'
}
Then add a new object to be given to the partial:
var model = {
some : 'attribute',
childContext : {
'another' : 'attribute' // this goes to the child partial
}
}
childContext becomes the context of the partial like Yehuda said -- in that, it only sees the field another, but it doesn't see (or care about the field some). If you had id in the top level model, and repeat id again in the childContext, that'll work just fine as the partial only sees what's inside childContext.
The accepted answer works great if you just want to use a different context in your partial. However, it doesn't let you reference any of the parent context. To pass in multiple arguments, you need to write your own helper. Here's a working helper for Handlebars 2.0.0 (the other answer works for versions <2.0.0):
Handlebars.registerHelper('renderPartial', function(partialName, options) {
if (!partialName) {
console.error('No partial name given.');
return '';
}
var partial = Handlebars.partials[partialName];
if (!partial) {
console.error('Couldnt find the compiled partial: ' + partialName);
return '';
}
return new Handlebars.SafeString( partial(options.hash) );
});
Then in your template, you can do something like:
{{renderPartial 'myPartialName' foo=this bar=../bar}}
And in your partial, you'll be able to access those values as context like:
<div id={{bar.id}}>{{foo}}</div>
Yes, I was late, but I can add for Assemble users: you can use buil-in "parseJSON" helper http://assemble.io/helpers/helpers-data.html. (Discovered in https://github.com/assemble/assemble/issues/416).
Not sure if this is helpful but here's an example of Handlebars template with dynamic parameters passed to an inline RadioButtons partial and the client(browser) rendering the radio buttons in the container.
For my use it's rendered with Handlebars on the server and lets the client finish it up.
With it a forms tool can provide inline data within Handlebars without helpers.
Note : This example requires jQuery
{{#*inline "RadioButtons"}}
{{name}} Buttons<hr>
<div id="key-{{{name}}}"></div>
<script>
{{{buttons}}}.map((o)=>{
$("#key-{{name}}").append($(''
+'<button class="checkbox">'
+'<input name="{{{name}}}" type="radio" value="'+o.value+'" />'+o.text
+'</button>'
));
});
// A little test script
$("#key-{{{name}}} .checkbox").on("click",function(){
alert($("input",this).val());
});
</script>
{{/inline}}
{{>RadioButtons name="Radio" buttons='[
{value:1,text:"One"},
{value:2,text:"Two"},
{value:3,text:"Three"}]'
}}

Categories