Ignoring undefined data / vars in an underscore template - javascript

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.

Related

How to substitute a series of keys into a string being displayed using javascript?

I have a set of templates that contain key phrases denoted by %%key%% (could use different delimiters if these are a problem). In the code presented, the names of the templates are shown in a selector, and when selected, their values are presently being moved into a textarea for display. Before they are displayed, I wish to go through the template and replace each key with the value associated with that key.
I have tried using template.gsub 'key' 'value', template.gsub! 'key' 'value', and even template['key'] = 'value', to no avail. To eliminate other problems, I have tried using simple values for the 'value' and then displaying the result in an alert. If I don't try the replacement, the alert shows the template. If I try any of these attempts, I don't get the alert to show, indicating some kind of javascript error, I suppose. I can't figure out what the error is.
Here is a part of the application_helper.rb:
#----------------------------------------------------------------------------
def render_haml(haml, locals = {})
Haml::Engine.new(haml.strip_heredoc, format: :html5).render(self, locals)
end
#----------------------------------------------------------------------------
def create_template_selector
get_templates()
render_haml <<-HAML
%select{{name: "msg", id: "template_selector"}}
- #t_hash.each do |name,message|
%option{ :value => message }= name
HAML
end
#----------------------------------------------------------------------------
def get_templates()
templates = Template.all
#t_hash = Hash.new
templates.each do |t|
#t_hash[t.name] = t.message
end
end
and this is the view partial _text.html.haml, where the selector is embedded and its selection is presented when changed:
= form_for(#comment, remote: true, html: {id: "#{id_prefix}_new_comment"}) do |f|
= hidden_field_tag "comment[commentable_id]", commentable.id, id: "#{id_prefix}_comment_commentable_id"
= hidden_field_tag "comment[commentable_type]", class_name.classify, id: "#{id_prefix}_comment_commentable_type"
%div
%h3 Select a Template
= create_template_selector
%div
= f.text_area :comment, id: "#{id_prefix}_comment_comment", name:"text_msg"
.buttons
= image_tag("loading.gif", size: :thumb, class: "spinner", style: "display: none;")
= f.submit t(:add_note), id: "#{id_prefix}_comment_submit"
#{t :or}
= link_to(t(:cancel), '#', class: 'cancel')
:javascript
$(document).ready( function() {
$('#template_selector').change(function() {
var data= $('select').find('option:selected').val();
//
// Here is where I put alert(data) and it works
// but,
// var filled = data.gsub '%%first_name%%' 'Ralph'
// followed by alert(filled), shows no alert panel and no error
//
$("##{id_prefix}_comment_comment").val(data);
});
} );
How can I create a set of fill_ins like:
def fill_ins()
#fillins = Hash.new
#fillins['%%first_name%%'] = 'Ralph'
#fillins['%%last_name%%'] = 'Jones'
...
end
and create a function like:
def fill_in(template)
#fi = fill_ins()
#fi.each do 'fkey, fval'
template.gsub! fkey fval
end
end
and have it work?
In :javascript block, you should be writing JavaScript, not Ruby.
String#gsub is a Ruby method.
String.prototype.replace is a JavaScript method.
var filled = data.replace(/%%first_name%%/g, 'Ralph');
EDIT: Forgot that replace with a string only replaces once. Use regular expression with global flag instead.
Also: to pass the data from Ruby to JavaScript, use this pattern in your template:
<script>
const fillIns = <%= fill_ins.to_json %>;
</script>
Then you can either loop that array and run the replace method with each pair (not optimal) — or you can use a regular expression that picks up on the general pattern of the variable:
var filled = data.replace(/%%([^%]+)%%/g, (_, name) => fillIns[name]);

JavaScript selecting Object Arraylike?

The Problem is the following:
I have a JSON file that has objects with the following name: "item0": { ... }, "item1": { ... }, "item2": { ... }. But I can't access them when going through an if method.
What I've done so far:
$.getJSON('/assets/storage/items.json', function(data) {
jsonStringify = JSON.stringify(data);
jsonFile = JSON.parse(jsonStringify);
addItems();
});
var addItems = function() {
/* var declarations */
for (var i = 0; i < Object.keys(jsonFile).length; i++) {
path = 'jsonFile.item' + i;
name = path.name;
console.log(path.name);
console.log(path.type);
}
}
If I console.log path.name it returns undefined. But if I enter jsonFile.item0.name it returns the value. So how can I use the string path so that it's treated like an object, or is there an other way on how to name the json items.
As others stated 'jsonFile.item' + i is not retrieving anything from jsonFile: it is just a string.
Other issues:
It makes no sense to first stringify the data and then parse it again. That is moving back and forth to end up where you already were: data is the object you want to work with
Don't name your data jsonFile. It is an object, not JSON. JSON is text. But because of the above remark, you don't need this variable
Declare your variables with var, let or const, and avoid global variables.
Use the promise-like syntax ($.getJSON( ).then)
Iterate object properties without assuming they are called item0, item1,...
Suggested code:
$.getJSON('/assets/storage/items.json').then(function(data) {
for (const path in data) {
console.log(data[path].name, data[path].type);
}
});
What you want is to use object notation using a dynamic string value as a key instead of an object key. So, instead of using something like object.dynamicName you either have use object[dynamicName].
So in your example it would be like this.
path = 'item' + i;
jsonFile[path].name
I'm afraid you cannot expect a string to behave like an object.
What you can do is this:
path = `item${i}`
name = jsonFile[path].name

How to use Ember's getters/setters on properties with dots?

In my webapp, I have a model with properties whose names are dynamically generated based on data from the server. For example, I would normally reference this by doing something like this from my controller:
var str1 = 'property.name.with.dots'; // String from server
this.get('model.someProperty')[str1].integer = 2;
this.get('model.someProperty')[str1].integer += 1;
But Ember doesn't like this - it says I should use a set or get function. Which makes sense. So I want to do something like this in place of the last line above:
this.get('model.someProperty.' + str1).incrementProperty('integer');
This would work fine out of the box if str1 didn't have dots. It does, though, so what can I do to get Ember's getters to work? I tried
this.get('model.someProperty')[str1].incrementProperty('integer');
but it doesn't work - the subobjects don't get Ember's methods by default.
Definitely
Massage the data before handing it off to Ember, having dots in your name will just cause a plethora of chaining problems.
Clean the data, I chose _ (this isn't deep cleaning, exercise for your fun)
App.cleanData = function(result){
var response = {},
re = new RegExp('\\.', 'g'),
newKey;
for(var key in result){
newKey = key.replace(re, '_');
response[newKey] = result[key];
}
return response;
};
Use the cleaned data instead of the server data
App.FooRoute = Em.Route.extend({
model: function(){
return $.getJSON('/foo').then(function(result){
return App.cleanData(result);
}
}
});

Underscore no with templates undefined variable when running test

I am using underscore templates to create some markup. I am using the no with approach so I am defining the variable setting.
So my template declarations look like so:
_.template('<div><%= data.title %></div>', { titles: title }, { variable: 'data'});
It works fine and evaluates the template fine, but when i run my unit test, it seems that, I get the error data is undefined.
By replacing data with either this, or self, or obj, it seems to work fine. I am wondering if there are any penalties to using those words like so:
_.template('<div><%= this.title %></div>', { titles: title }, { variable: 'this'});
OR
_.template('<div><%= this.title %></div>', { titles: title }, { variable: 'obj'});
OR
_.template('<div><%= self.title %></div>', { titles: title }, { variable: 'self'});
Thanks for the info.
You are expecting 'titles' as the key in your object, whereas the key that object contains is 'title'. Also, you need not pass the third parameter to _.template.
Since _.template returns a function, you can directly call this function by passing the object it needs as parameter as shown below:
Working Demo
JavaScript:
var temp = {"title": "Hello world!"};
/* Approach 1
var t = _.template('<div><%= title %></div>');
document.getElementById("output").innerHTML = t(temp);
*/
/* Approach 2 */
document.getElementById("output").innerHTML = _.template('<div><%= title %></div>', temp);

Need Handlebars.js to render object data instead of "[Object object]"

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>

Categories