I am trying to implement a Handlebars helper that will take in some data, and based on that data generate a list. After the list was generated in a helper method, I'd want to loop this list. It will be clear in the example.
The scenario is: I have some data, and as I parse through the data I want to create some custom warnings for inconsistency in that data. The method responsible to generate the warnings from raw JSON data is called generateWarnings().
I have functions to compile template and render the data, defined like this:
function compileTemplate(targetId) {
return Handlebars.compile($("#" + targetId).html());
}
function renderData(template, context, targetId) {
$("#" + targetId).html(template(context));
}
I am calling them:
var warningsPlaceHolderId = "warnings_plholder";
var warningsTemplateId = "warnings_template";
var warningsTemplate = HandlebarsHelpers.compileTemplate(warningsTemplateId);
renderData(warningsTemplate, data, warningsPlaceHolderId);
Handlebars template:
<ol>
{{#each generateWarnings this}}
Warning description: {{this.text}}
{{/each}}
</ol>
Here, generateWarnings helper is supposed to take my raw JSON data, create the actual list of warnings, and display each of this warnings as the element in the HTML list.
I registered the Helper:
Handlebars.registerHelper('generateWarnings', function(data) {
return generateWarnings(data);
});
generateAll: function(data) {
var warning1 = {
type: 'error',
text: 'Testing error!'
};
var warning2 = {
type: 'warning',
text: 'Testing warning!'
};
var warning3 = {
type: 'info',
text: 'Testing info'
};
var arr = [];
arr.push(warning1);
arr.push(warning2);
arr.push(warning3);
return arr;
}
So I would expect the text property of these 3 warnings to be in a list, but instead I am getting an error:
Uncaught TypeError: inverse is not a function at renderData().
Shouldn't this be using generateAll() in your helper?
Handlebars.registerHelper('generateWarnings', function(data) {
return generateWarnings(data); // THIS SHOULD BE generateAll(data)??
});
generateAll: function(data) {
Also, you are using a label?! for your function which won't be callable the way you expect.
This:
generateAll: function(data) { ... };
should be:
var generateAll = function(data ) { ... };
I have never seen a good use for labels in JavaScript.
Your approach seems to be a bit unusual (and overly complicated) as well. Why not just manipulate the data before passing it to your template? I would consider using JavaScript to generate the warnings, then attach them to your data before passing to your template. I don't see a particular reason to be using a Handlebars helper in this situation (granted, I'm not totally familiar with the problem you are trying to solve).
Related
The ultimate goal is to detect changes between an existing Parse object and the incoming update using the beforeSave function in Cloud Code.
From the Cloud Code log available through parse.com, one can see the input to beforeSave contains a field called original and another one called update.
Cloud Code log:
Input: {"original": { ... }, "update":{...}
I wonder if, and how, we can access the original field in order to detect changing fields before saving.
Note that I've already tried several approaches for solving this without success:
using (object).changedAttributes()
using (object).previousAttributes()
fetching the existing object, before updating it with the new data
Note on request.object.changedAttributes():
returns false when using in beforeSave and afterSave -- see below for more details:
Log for before_save -- summarised for readability:
Input: { original: {units: '10'}, update: {units: '11'} }
Result: Update changed to { units: '11' }
[timestamp] false <--- console.log(request.object.changedAttributes())
Log for corresponding after_save:
[timestamp] false <--- console.log(request.object.changedAttributes())
There is a problem with changedAttributes(). It seems to answer false all the time -- or at least in beforeSave, where it would reasonably be needed. (See here, as well as other similar posts)
Here's a general purpose work-around to do what changedAttributes ought to do.
// use underscore for _.map() since its great to have underscore anyway
// or use JS map if you prefer...
var _ = require('underscore');
function changesOn(object, klass) {
var query = new Parse.Query(klass);
return query.get(object.id).then(function(savedObject) {
return _.map(object.dirtyKeys(), function(key) {
return { oldValue: savedObject.get(key), newValue: object.get(key) }
});
});
}
// my mre beforeSave looks like this
Parse.Cloud.beforeSave("Dummy", function(request, response) {
var object = request.object;
var changedAttributes = object.changedAttributes();
console.log("changed attributes = " + JSON.stringify(changedAttributes)); // null indeed!
changesOn(object, "Dummy").then(function(changes) {
console.log("DIY changed attributes = " + JSON.stringify(changes));
response.success();
}, function(error) {
response.error(error);
});
});
When I change someAttribute (a number column on a Dummy instance) from 32 to 1222 via client code or data browser, the log shows this:
I2015-06-30T20:22:39.886Z]changed attributes = false
I2015-06-30T20:22:39.988Z]DIY changed attributes =
[{"oldValue":32,"newValue":1222}]
When running the following from the UserController on Google Chrome, with ember-couchdb-kit-0.9, Ember Data v1.0.0-beta.3-56-g8367aa5, Ember v1.0.0, and this couchdb adapter:
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir',
user: model
});
customer.save().then(function() {
model.set('customer', customer);
model.save();
});
}
with these models:
App.User = App.Person.extend({
name: DS.attr('string'),
customer: DS.belongsTo('customer', {async: true })
App.Customer = DS.Model.extend({
user: DS.belongsTo('user', {async: true}),
description: DS.attr('string')
});
neither the user nor the customer has their relationship set properly (in the Ember Debugger the user has null and the customer has <computed>, rather than some sort of <EmberPromiseObject> which is what they have when it works).
This only happens when the object in question is persisted. If the save() calls are omitted, both have correctly set relationships, but of course the database hasn't been updated with this information. Whenever the saves happen, the relationships are overwritten with empty entries.
I found that the problem was in the adapter's serializeBelongsTo function, which I've now changed my copy to the following:
serializeBelongsTo: function(record, json, relationship) {
console.log("serializeBelongsTo");
console.log(record.get('user'));
console.log(json);
console.log(relationship);
var attribute, belongsTo, key;
attribute = relationship.options.attribute || "id";
console.log(attribute);
key = relationship.key;
console.log(key);
belongsTo = Ember.get(record, key);
console.log(belongsTo);
if (Ember.isNone(belongsTo)) {
return;
}
json[key] = Ember.get(belongsTo, attribute);
console.log(Ember.get(belongsTo, attribute));
console.log(json);
if (relationship.options.polymorphic) {
return json[key + "_type"] = belongsTo.constructor.typeKey;
}
else {
return json;
}
}
attribute, belongsTo, and key all log as correct, but
console.log(Ember.get(belongsTo, attribute)); returns undefined,
which I've tried to change to
console.log(Ember.get(Ember.get(belongsTo, 'content'), attribute));
since console.log(belongsTo); told me the id attribute was hidden inside a content object. Attached is a screenshot showing what I mean.
The change doesn't fix the problem though, and I keep getting undefined. No matter what method I use to try to get the id out of the belongsTo object, I always get either null or undefined. Here are some examples of things I've tried to get content out of the object:
var content = belongsTo.content;
var content = Ember.get(belongsTo, 'content');
var content = belongsTo.get('content');
console.log(json); returns Object {description: "Why hello sir", user: undefined}
Here's a pastebin showing relevant output: http://pastebin.com/v4mb3PJ2
Update
A very confusing update!
When I save the model from a different function:
saveModel: function() {
this.get('model').save().then(
function( data, textStatus, jqXHR ) {
console.log('Saved successfully.');
},
function( jqXHR, textStatus, errorThrown ) {
console.log(jqXHR);
console.log(errorThrown);
console.log(textStatus);
}
);
}
The model is correctly saved. Everything in serializeBelongsto works exactly as expected.
Here's a different pastebin showing output for this case: http://pastebin.com/Vawur8Q0
I figured out the problem. Basically the belongsTo object in serializeBelongsTo wasn't really resolved by the time it was being referenced, which I found out by querying isFulfilled. So I implemented by saving side this way:
function saveOn (target, attribute) {
target.addObserver(attribute, function () {
if (target.get(attribute)) {
console.log("Inside with %#".fmt(attribute));
target.removeObserver(attribute);
Ember.run.once(target, function() {
target.save();
});
}
});
};
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir'
});
customer.save().then(function () {
model.set('customer', customer);
customer.set('user', model);
saveOn(customer, 'user.isFulfilled');
saveOn(model, 'customer.isFulfilled');
});
}
Now everything works like a charm. It might be a good idea for serializeBelongsTo to take this into account though. This line: console.log(Ember.get(belongsTo, 'isFulfilled')); was coming up false in my case. There was just a race condition of some sort between the creation of the record and it's serialization!
I'd like to make my saveOn function return a promise though, which I could then use to chain multiple saveOns together. That way I wouldn't have to do a customer.save() to make sure the id's were populated.
Let me explain my issue, I am trying to populate Ember.Select directly from database.
I have these routes:
this.resource('twod', function() {
this.resource('twoduser', {
path : ':user_id'
});
});
In twoduser, I am displaying a full information about a single user. In that view, I have a Select Box as well, which end user will select and then with a button, he can add the user to a team that he selected from Ember.Select.
I tried to do this,
App.TwoduserController = Ember.ArrayController.extend({
selectedTeam : null,
team : function (){
var teams = [];
$.ajax({
type : "GET",
url : "http://pioneerdev.us/users/getTeamNames",
data : data,
success : function (data){
for (var i = 0; i < data.length; i ++){
var teamNames = data[i];
teams.push(teamNames);
}
}
});
return teams;
}.property()
})
Then in my index.html:
{{view Ember.Select
contentBinding="team"
optionValuePath="teams.team_name"
optionLabelPath="teams.team_name"
selectionBinding="selectedTeam"
prompt="Please Select a Team"}}
But when I do this, for some reason it interferes with Twoduser and I am not able to view the single user.
Furthermore, here's a sample JSON response I will get through the url:
{"teams":[{"team_name":"Toronto Maple Leafs"},{"team_name":"Vancouver Canuck"}]}
Moreover, I am fetching all users using Ajax like this:
App.Twod.reopenClass({
findAll : function() {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.getJSON("http://pioneerdev.us/users/index", function(data) {
var result = data.users.map(function(row) {
return App.Twod.create(row);
});
resolve(result);
}).fail(reject);
});
},
findBy : function(user_id) {
return new Ember.RSVP.Promise(function(resolve, reject) {
var user = App.Twod.create();
$.getJSON("http://pioneerdev.us/users/byId/" + user_id, function(data) {
var result = user.setProperties(data.user);
resolve(result);
}).fail(reject);
});
}
});
Though there's one thing, I have a separate Teams route:
this.resource('teamview', function(){
this.resource('teamviewdetail', {
path : ':team_id'
});
});
Which shows all the teams and a single team when you click on a single team.
Can I use that TeamviewController? or Can I fetch team names from Twoduser Controller and push names to the array as I mentioned before?
More Information:
If I use the way I mentioned, I get this error:
Uncaught TypeError: Object [object Object] has no method 'addArrayObserver'
Here's a working jsfiddle on the issue I am experiencing. You can select "Storyboard" from the Designation & then select the user. That will reproduce the issue.
One more Update: Seems using ObjectController instead of ArrayController issue solves the addArrayObserver issue. But still I can't get the teams in the Ember.Select.
The biggest issue here is that you use Array#push instead of pushObject. Ember needs the special methods in order to be aware of changes. Otherwise, it will continue to think that the array of teams is as empty as when you first returned it. Second biggest issue is that your ajax success call isn't accessing the returned data properly.
Also, optionValuePath and optionLabelPath are relative to the individual select option view, so they should start with content, which is the individual item as set on the view. So: content.team_name
I am trying to parse a multilevel json file, create a model and then add that model to a backbone collection but i can't seem to figure out how to push the model to the collection. This should be a pretty easy problem to solve, i just can't seem to figure it out. Thanks in advance for your help. Below is my model and collection code:
var Performer = Backbone.Model.extend({
defaults: {
name: null,
top5 : [],
bottom5 : []
},
initialize: function(){
console.log("==> NEW Performer");
// you can add event handlers here...
}
});
var Performers = Backbone.Collection.extend({
url:'../json_samples/performers.json',
model:Performer,
parse : function(data) {
// 'data' contains the raw JSON object
console.log("performer collection - "+data.response.success);
if(data.response.success)
{
_.each(data.result.performers, function(item,key,list){
console.log("running for "+key);
var tmpObject = {};
tmpObject.name = key;
tmpObject.top5 = item.top5;
tmpObject.bottom5 = item.bottom5;
var tmpModel = new Performer(tmpObject);
this.models.push(tmpModel);
});
}
else
{
console.log("Failed to load performers");
}
}
});
As has been said in comments to your question, parse() is not intended to be used this way. If data.results.performers was an Array, all you would have to do is returning it. In your case the code will be slightly different.
var Performers = Backbone.Collection.extend({
...
parse: function(resp, options) {
return _.map(resp.result.performers, function(item, key) {
return _.extend(item, {name: key});
});
}
...
});
On the advice side, if you have the chance to change the API server-side, you'd probably be better off treating collections of objects as arrays and not as objects. Even if it is sometimes convenient to access an object by some ad-hoc key, the data really is an array.
You'll be able to transform it later when you need performers-by-name with a function like underscore's IndexBy
I'm creating a web app that uses MONGOHQ to store data, and that uses Sinatra to run the app. If I go to: localhost:4578/names.json, I get the names of all the names that I use for my data. However, I'm having trouble accessing this data using the getJSON method of jquery.
The file /names.json looks like this:
["Yasiel Puig","Nick Franklin","Mike Zunino","Jurickson Profar","Manny Machado"]
I tried doing something like this:
var series = []
$.get('names.json', function(n) {
n.forEach(function(s) {
series.push({
name: s
})
})
}, 'json')
But this does not really work. Do you have any other ideas for how I should access the json data? Could I save it to a var? I'm pretty sure the json data is not JSONP format, so maybe I should treat it as AJAX?
Your code seems to work, I tried it in this Fiddle. Therefore the problem is probably on server side.
var data = ["Yasiel Puig", "Nick Franklin", "Mike Zunino",
"Jurickson Profar", "Manny Machado"];
var series = [];
data.forEach( function( e ) {
series.push( {
name: e
});
}
);
series.forEach( function( e ) {
console.log( e.name );
});
there is a difference between calling $.get('names.json') and $.get('/names.json') I think you are not adding the starting slash(/) to the url
when you call $.get('names.json') it calls complete_current_url + '/names.json'
eg. if you are on /home page then the url that would be called is /home/names.json
and $.get('/names.json') will call current_domain + '/names.json'
from any page it will always call '/names.json'
Could I save it to a var?
Possibly. Though, it depends on which variable and where/when you need it.
$.get() is asynchronous. It only starts the request, sets the callback as a listener, and then exits.
var series = [];
$.get('names.json', function (n) {
n.forEach(function(s) {
series.push({
name: s
});
});
// inside the callback...
console.log(series); // filled: [ { name: "Yasiel Puig" }, ... ]
});
// after the request...
console.log(series); // still empty: []
So, you can use series, or more importantly n, within the callback. Outside, it won't be available yet.