I'm using Handlebars templates and JSON data is already represented in [Object object], how do I parse this data outside of the Handlebars? For example, I'm trying to populate a JavaScript variable on the page through a handlebars tag, but this doesn't work.
Any suggestions? Thank you!
EDIT:
To clarify, I'm using ExpressJS w/ Handlebars for templating. In my route, I have this:
var user = {}
user = {'id' : 123, 'name' : 'First Name'}
res.render('index', {user : user});
Then in my index.hbs template, I now have a {{user}} object. I can use {{#each}} to iterate through the object just fine. However, I'm also using Backbonejs and I want to pass this data to a View, such as this:
myView = new myView({
user : {{user}}
});
The problem is that {{user}} simply shows [Object object] in the source. If I put it in console.log I get an error that says 'Unexpected Identifier'.
When outputting {{user}}, Handlebars will first retrieve the user's .toString() value. For plain Objects, the default result of this is the "[object Object]" you're seeing.
To get something more useful, you'll either want to display a specific property of the object:
{{user.id}}
{{user.name}}
Or, you can use/define a helper to format the object differently:
Handlebars.registerHelper('json', function(context) {
return JSON.stringify(context);
});
myView = new myView({
user : {{{json user}}} // note triple brackets to disable HTML encoding
});
You can simple stringify the JSON:
var user = {}
user = {'id' : 123, 'name' : 'First Name'};
// for print
user.stringify = JSON.stringify(user);
Then in template print by:
{{{user.stringify}}};
I'm using server-side templating in node-js, but this may apply client-side as well. I register Jonathan's json helper in node. In my handler, I add context (such as addressBook) via res.locals. Then I can store the context variable client-side as follows:
<script>
{{#if addressBook}}
console.log("addressBook:", {{{json addressBook}}});
window.addressBook = {{{json addressBook}}};
{{/if}}
</script>
Note the triple curlies (as pointed out by Jim Liu).
You are trying to pass templating syntax {{ }} inside a JSON object which is not valid.
You may need to do this instead:
myView = new myView({ user : user });
In the Node Router - Stringify the response object. See below line.
response.render("view", {responseObject:JSON.stringify(object)});
In HTML Script tag - user Template literals (Template strings) and use JSON.parse.
const json= `{{{responseObject}}}`;
const str = JSON.parse(json);
Worked like a charm!
You can render the keys/values of a list or object in a Handlebars template like this:
{{#each the_object}}
{{#key}}: {{this}}
{{/each}}
If you want more control over the output formatting you can write your own helper. This one has a recursive function to traverse nested objects.
Handlebars.registerHelper('objToList', function(context) {
function toList(obj, indent) {
var res=""
for (var k in obj) {
if (obj[k] instanceof Object) {
res=res+k+"\n"+toList(obj[k], (" " + indent)) ;
}
else{
res=res+indent+k+" : "+obj[k]+"\n";
}
}
return res;
}
return toList(context,"");
});
We used handlebars for email templates and this proved useful for a user like the following
{
"user": {
"id": 123,
"name": "First Name",
"account": {
"bank": "Wells Fargo",
"sort code": " 123-456"
}
}
}
To condense (what I found to be) the most helpful answers...
JSON helper for handlebars (credit):
Handlebars.registerHelper("json", function (context) {
return JSON.stringify(context);
});
JSON helper for express-handlebars (credit and I updated to newest conventions):
app.engine(
"handlebars",
exphbs.engine({
defaultLayout: "main",
helpers: {
json: function (context) {
return JSON.stringify(context);
}
}
})
);
And then on the templating side: {{json example}}
Just improving the answer from #sajjad.
Adding a 'pre' tag will make it look a lot nicer.
<pre>
{{#each the_object}}
{{#key}}: {{this}}
{{/each}}
</pre>
Related
In my Nodejs site, I created a JSON array and sent it to the angularJs client. Then it shows as [object, object]. Because of that reason, I used JSON.stringify now it shows in a weird way in angularJs client.
Array creation in NodeJs side,
getAttachments = function () {
let attach = [];
workflowSession.files.forEach(file => {
if (file) {
const item = {
text: file.name,
link: workflowSession.metadata.portalBaseUrl + "api/files/" + workflowSession.workflowRef + "#"+ file.name
}
attach.push(item);
}
});
return attach;
};
When I put the console log in the Nodejs side it displayed as below,
[ { text: 'sdee.png',
link: 'http://localhost:3000/api/files/kYnDsXjyFT#sdee.png' },
{ text: 'wes.png',
link: 'http://localhost:3000/api/files/kYnDsXjyFT#wes.png' }
]
But in AngularJs client shows it as [object, object], because of that reason I made the changes to the above array creation method as below,
getAttachments = function () {
let attach = [];
workflowSession.files.forEach(file => {
if (file) {
const item = {
text: file.name,
link: workflowSession.metadata.portalBaseUrl + "api/files/" + workflowSession.workflowRef + "#"+ file.name
}
attach.push(item);
}
});
return JSON.stringify(attach);
};
Then NodeJs side shows data for console.log as below,
[
{"text":"sdee.png","link":"http://localhost:3000/api/files/kYnDsXjyFT#sdee.png"}, {"text":"wes.png","link":"http://localhost:3000/api/files/kYnDsXjyFT#wes.png"}
]
But in AngularJs side shows it in the as below,
[
{"text":"wes.png","link":"http://localhost:3000/api/files/saJiCDZPet#wes.png"},{"text":"ssdd.png","link":"http://localhost:3000/api/files/saJiCDZPet#ssdd.png"}
]
I want to iterate this array as shown in below HTML snippet, but I can't do that because of this format issues. The "action.links" is the array created from the Nodejs side.
<md-menu-content>
<md-menu-item ng-repeat="item in action.links" ng-if="item.link">
<md-button href="{{item.link}}" target="_blank">
{{item.text}}
</md-button>
</md-menu-item>
</md-menu-content>
What am I doing wrong?
Looks like the Angular side is getting HTML encoded, what you could do is run it through an HTML decode:
var htmlDecode = function(str) {
var temp = document.createElement('span');
temp.innerHTML = str;
return temp.textContent;
};
var foo = '[{"text":"wes.png","link":"http://localhost:3000/api/files/saJiCDZPet#wes.png"},{"text":"ssdd.png","link":"http://localhost:3000/api/files/saJiCDZPet#ssdd.png"}]';
console.log(
htmlDecode(foo)
);
Ideally though you would keep it from getting HTML encoded in the first place. If you're able to share a live version of your code it would be easier to debug why this is happening.
You don't need to stringify the JS object on the node.js side. Based on what you've provided, the [object, object] syntax you're seeing when logging in the console may be okay. Did iteration over the array not work when it was being returned as an array of objects?
If you want to see the data using console.log on the client side, stringify the object there:
console.log(JSON.stringify(action.links));
You will not be able to iterate over a JSON array because it's a string and would need to be parsed back to an array of objects first.
I have a json object in collection which I need to show it on the page.
Here is what I did:
I first call the helpers template then in that I fetch the json object from the collection:
I am using coffeescirpt and jade-handlebars, here goes my code in coffeescript:
Template.test.helpers
test: ->
test = Question.find().fetch();
test
In the console when I do Question.find().fetch() the following thing occurs:
QuestionData: Object
question1: "How many kids do I have ?"
question2: "when will i die ?"
question3: "how many wife do i have ?"
question4: "test"
__proto__: Object
_id: "w9mGrv7LYNJpQyCgL"
userid: "ntBgqed5MWDQWY4xt"
specialNote: "Rohan ale"
Now in the jade when I call the template by:
template(name="hello")
.test {{QuestionData}}
I can see only the [object] [object]. To see the question1,question2 I have to do the following:
template(name="hello")
.test {{QuestionData.question1}}, {{QuestionData.question2}}
How can I dynamically show all the questions without doing {{QuestionData.question1}} ...
Thank You in advance !!!
You can dynamically compose field names in a loop.
b = { q1: 'a1', q2: 'a2', q3: 'a3' };
for (x=1;x<=3;x++) { console.log( b[ 'q' + x ] ) }
That being said, there's a lot here that seems a misunderstanding to me. I'd step back and say that you should look into storing one question per mongo document. This gives you the easiest data for meteor to play with. Or, storing multiple questions in an array:
test = {
questions : [
"How many kids do I have ?"
"when will i die ?"
"how many wife do i have ?"
"test" ] ,
userid: "ntBgqed5MWDQWY4xt",
specialNote: "Rohan ale"
}
The problems come when you think how you store the answers, sort the questions, etc. Probably a collection called questions, with a field maybe called sortOrder, a field called tag, etc.
How did you pick up calling templates this way, rather than having them as html files that a router manages for you?
instead of just returning your json object with Questions.find().fetch() you could add another step to put your data into an array like:
test = function() {
var temp = [];
for (item in Questions.find().fetch()) {
temp.push(item);
};
return temp;
};
return test;
(sorry for not writing in coffee script, i'm not aware of the language abstraction)
To answer your question on how to do it, you can do something like this(in JS, sorry, not a coffeeScripter):
Template.Questionnaire.questions = function () {
var questions = [];
_.each(Object.keys(this), function (field) {
if(/^question.+/.test(field)) {
questions.push({ label: field, question: this[field]});
}
});
return questions;
};
And then in a template:
<template name="Questionnaire">
{{#each questions}}
<label>{{label}}</label>
<span>{{question}}</span>
{{/each}}
</template>
Something like that. But I agree with Jim Mack and that you should probably be storing this in an array.
Like as JIm Mack Posted, save your collection in an array
first of all insert your question in an array by doing these in your coffeescript:
x = document.getElementById('question-form')
length = x.length
i = 0
question = []
while i< length
aaa = x.elements[i]
question.push
questions: aaa
i++
then since you are using Jade-handlebars you need register helpers
in your jade file do these
{{#each arrayify myObject}}
{{#each this.name}}
p {{questions}}
{{/each}}
{{/each}}
The arrayify and myObject are the handlebars helpers. Then in your coffeescript
Handlebars.registerHelper "arrayify", (obj) ->
result = []
for temp in obj
userQuestion = temp.question
result.push
name: userQuestion
return result
Template.templatename.myObject = ->
temp = []
for item in Question.find().fetch()
temp.push item
return temp
Hope these will work.
In my grails controller:
assert result == [hus:['hus#gmail.com', 'SE', 'on', '9908899876'], vin:['vin#gmail.com', 'SD', 'on', '7765666543']]
println "result is::"+result
println result.getClass()
[result:result] //passing model to view
which prints :
[hus:[hus#gmail.com, SE, on, 9908899876], vin:[vin#gmail.com, SD, on, 7765666543]]
class java.util.LinkedHashMap
but when i get this hashmap from my view page and access from javascript
$(function(){
alert('${result}');
});
it prints
{
hus=[hus#gmail.com,SE,9902766542],
vin = [vin#gmail.com, SE,887654433]
}
which is not valid object , is not a valid object, (: replaced by =) it should be
{ hus:[hus#gmail.com,SE,9902766542], vin : [vin#gmail.com, SE,887654433] }
why it is so? how do i correct it?
Your current output is produced by toString() of the Map result which can be used to
print the data but is not very useful to transfer it to javascript.
You should convert the result to json in your controller:
def json = result as JSON
and return it to your model:
[json : json ]
After that you can create an object in Javascript using JQery.parseJSON:
var obj = jQuery.parseJSON("${ json.toString() }");
Still learning backbone so bear with me;
I'm trying to add a new model with blank fields to a view, but the template I've created has a whole bunch of
<input value="<%= some_value %>" type="whatever" />
Works perfectly fine when fetching data, it populates it and all goes well. The trouble arises when I want to create a new (blank) rendered view, it gives me
Uncaught ReferenceError: some_value is not defined
I can set defaults (I do already for a few that have default values in the db) but that means typing out over 40 of them with blanks; is there a better way of handling this?
I'm fiddling around with the underscore template itself, trying something like <%= if(some_value != undefined){ some_value } %> but that also seems a bit cumbersome.
Pass the template data inside a wrapper object. Missing property access won't throw an error:
So, instead of:
var template = _.template('<%= foo %><%= bar %>');
var model = {foo:'foo'};
var result = template(model); //-> Error
Try:
var template = _.template('<%= model.foo %><%= model.bar %>');
var model = {foo:'foo'};
var result = template({model:model}); //-> "foo"
Actually, you can use arguments inside of your template:
<% if(!_.isUndefined(arguments[0].foo)) { %>
...
<% } %>
No,
There is no actual fix for this due to the way underscore templates are implemented.
See this discussion about it:
I'm afraid that this is simply the way that with(){} works in JS. If the variable isn't declared, it's a ReferenceError. There's nothing we can do about it, while preserving the rest of template behavior.
The only way you can accomplish what you're looking for is to either wrap the object with another object like the other answer suggested, or setting up defaults.
If you check source code for generated template function, you will see something like this:
with (obj||{}) {
...
// model property is used as variable name
...
}
What happens here: at first JS tries to find your property in "obj", which is model (more about with statement). This property is not found in "obj" scope, so JS traverses up and up until global scope and finally throws exception.
So, you can specify your scope directly to fix that:
<input value="<%= obj.some_value %>" type="whatever" />
At least it worked for me.
Actually, you can access vars like initial object properties.
If you'll activate debugger into template, you can find variable "obj", that contains all your data.
So instead of <%= title %> you should write <%= obj.title %>
lodash, an underscore replacement, provides a template function with a built-in solution. It has an option to wrap the data in another object to avoid the "with" statement that causes the error.
Sample usage from the API documentation:
// using the `variable` option to ensure a with-statement isn’t used in the compiled template
var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' });
compiled.source;
// → function(data) {
// var __t, __p = '';
// __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!';
// return __p;
// }
A very simple solution: you can ensure that your data collection is normalized, i.e. that all properties are present in each object (with a null value if they are unused). A function like this can help:
function normalizeCollection (collection, properties) {
properties = properties || [];
return _.map(collection, function (obj) {
return _.assign({}, _.zipObject(properties, _.fill(Array(properties.length), null)), obj);
});
}
(Note: _.zipObject and _.fill are available in recent versions of lodash but not underscore)
Use it like this:
var coll = [
{ id: 1, name: "Eisenstein"},
{ id: 2 }
];
var c = normalizeCollection(coll, ["id", "name", "age"]);
// Output =>
// [
// { age: null, id: 1, name: "Eisenstein" },
// { age: null, id: 2, name: null }
// ]
Of course, you don't have to transform your data permanently – just invoke the function on the fly as you call your template rendering function:
var compiled = _.template(""); // Your template string here
// var output = compiled(data); // Instead of this
var output = compiled(normalizeCollection(data)); // Do this
You can abstract #Dmitri's answer further by adding a function to your model and using it in your template.
For example:
Model :
new Model = Backbone.Model.extend({
defaults: {
has_prop: function(prop) {
return _.isUndefined(this[property]) ? false : true;
}
}
});
Template:
<% if(has_prop('property')) { %>
// Property is available
<% } %>
As the comment in his answer suggests this is more extendable.
I want to use handlebars #each with an object that's not an array.
How do I do that? I need it to still work with meteor's special features with #each.
My object is in the form of:
{
john: "hello",
bob: "hi there"
}
I'm trying to get an output like this:
<div>hello</div>
<div>hi there</div>
You need to use a helper in your js to help handlebars understand your object:
Add to your client js
Template.registerHelper('arrayify',function(obj){
var result = [];
for (var key in obj) result.push({name:key,value:obj[key]});
return result;
});
And use (you can also use the key with {{name}}) in your html:
{{#each arrayify myobject}}
<div title="hover here {{name}}">{{value}}</div>
{{/each}}
myobject comes from your template:
Template.templatename.helpers({
myobject : function() {
return { john:"hello", bob: "hi there" }
}
});
You can convert your object into array with underscore _.map
html:
<template name="test">
{{#each person}}
<div>{{greeting}}</div>
{{/each}}
</template>
js:
Template.test.helpers({
person : function () {
return _.map(object, function(val,key){return {name: key, greeting: val}});
}
});
It should be noted for people finding this now that the correct way to declare Handlebars helpers in Meteor as of this writing is with the UI.registerHelper method as opposed to Handlebars.registerHelper. So the above helper should look like this now:
UI.registerHelper("arrayify", function(obj){
result = [];
for (var key in obj){
result.push({name:key,value:obj[key]});
}
return result;
});