serializeData function vs Marionette.renderer for custom data - javascript

I've noticed Marionette is very un-opinionated as far things go for the freedom they give you to choose a method to render data. It seems like there are a lot of ways to initially render a template with custom data
Returning a template with data:
template: function () {
var myTemplate = $('.someTemplate')
return _.template(myTemplate.html())({some: data});
}
Very similarly:
render: function () {
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, {
model: this.model.toJSON(),
customData: this.customData
});
this.$el.html(html);
}
Serialize data:
serializeData : function () {
var customData = {some: 'data'};
var model = this.model.toJSON()
return _.extend(customData, model);
}
I've seen a lot of people in different code use variations of the first and the second. I personally prefer using serializeData but am curious: is there an advantage or use case where it would be appropriate to use the first two methods instead of serializeData?

The first case is not efficient - you are recompiling the template every time you want to render.
Anyway, your use case is exactly why Marionette has templateHelpers. Its the most concise way to provide extra data to the template while also passing serialized model.
So you would write:
templateHelpers : function () {
return {some: 'data'};
}
Or if its just static stuff:
templateHelpers: {some: 'data'}
More examples on how to use it here.

I think it's all about exploring natural behaviour of these things. Backbone View render is empty function by default. Marionette ItemView render extends Backbone's with this code.
It takes template by getTemplate method, by default it gives what is stored in template option. You can override getTemplate if you want to choose between several templates.
Then it collects data needed to be rendered by runing serializeData and extending it with templateHelpers. First one by default returns your model or collection toJSON method result, there you can prepare your data some way on every render. Second one is for helpers that will be calculated (if they are functions) if needed in template.
Template and data then go to Marionette.Renderer where just return template(data) by default happens. And then result can be attached to view's element.

Related

Rendering more than just model attributes in Parse JS API (backbone.js) view templates

So I'm trying to use a join table to display a list of data in my Parse app. The javascript API is similar enough to backbone.js that I'm assuming anyone who knows that could help me. I can't show my actual source code but I think I simple twitter-like "user follows user" scenario can answer my question. So assume I have a join table called "follows" that simply contains its own objectId, the id of each user in the relationship, and some meta-data about the relationship (needing metadata is why I'm using a join table, instead of Parse.Relation). I want to have a view that finds all of the users the current user follows and renders an instance of another view for each case. From what I have so far, that would looks something like this.
In the intialize of the top level view (let's call it AllFollowsView), I would have something like this.
var currentUser = Parse.User.current();
var followsQuery = new Parse.Query(Follows);
followsQuery.equalTo("userId", currentUser.id);
followsQuery.find({
success: function(followsResult){
for (var i = 0; i < followsResult.length; i++){
var view = new OneFollowView({model:followsResult[i]});
this.$("#followed-list").append(view.render().el);
}//for loop
},
error: function(error){
console.log("error finding plans query");
}
});
OneFollowsView is just a view that renders an showing data about the relationship and listens for changes on that particular relationship (mainly change or delete in my case). I understand that by passing in the corresponding model with
var view = new OneFollowView({model:followsResult[i]});
I can print out attributes of that model in the OneFollowsView template like this
<li>You are following a user with the id of <%= _.escape(followedUserId) %></li>
My problem is that this only gives me access to the information stored in the "follows" object. How would I pass in the corresponding user models (or any other models that I can query for the id of) into the template so I can access them in the html in the same way. I would like to be able to run queries in one of the views and then access those models in the html. I know I can add attributes to the object before declaring a new instance of the lower level class with that object as the model, but that doesn't help me because I don't want to save it with new attributes attached.
EDIT: My render function for the top level function is empty at the moment. It's initilize function contains this line to render the template. I guess this should probably be in the render function and then I would call render from initialize.
this.$el.html(_.template($("#all-follows-template").html()));
Here's the render for the lower (individual li) view
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
this.delegateEvents();
}
From my understanding this just renders the template to el while parsing the model to JSON and then returns to allow chained calls.
The problem here lies in you render method. When you call this.template in your render method. That method, this.template is a template function returned by calling the _.template function. When you call your this.template method, the properties of the object you pass in will be available as instance variables in your template.
In your case you're passing in the JSON of the object. So, the properties of the model become names of variables available in your template. If you want to expose additional variables to the template you have a couple options: 1) Add to the jsonified model's attributes. 2) Send in the model as a top level variable and any additional variables you may want.
// option 1
render: function() {
var templateArgs = _.extend(this.model.toJSON(), { additionalVar: 'new var' });
var content = this.template(templateArgs);
$(this.el).html(content);
this.delegateEvents();
return this;
}
// option 2
render: function() {
var templateArgs = {
followResult: this.model.toJSON(),
additionalVar: 'new var'
};
var content = this.template(templateArgs);
$(this.el).html(content);
return this;
this.delegateEvents();
}
Either option is reasonable. I would probably go with option 2. Which allows you in the template to say something like:
<li> <%= followResult.someProperty %> <%= additionalVar %> </li>
Hope that helps. :)

In Backbone, how do I have an after_render() on all views?

I am maintaining a javascript application and I would like there to be a jquery function invoked on pretty much every view. It would go something like this:
SomeView = Backbone.Marionette.ItemView.extend
initialize: ->
#on( 'render', #after_render )
after_render: ->
this.$el.fadeOut().fadeIn()
Clearly there is a better way to do this than have an after_render() in each view? What is the better way to do it? If you can give an answer that includes jasmine tests, I'll <3 you ;)
The event you are looking for is onDomRefresh. See here for the documentation:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md#view-domrefresh--ondomrefresh-event
Create your own base view class and put your afterRender code in it. When you create a view, inherit from this class.
var MyApp.ItemView = Backbone.Marionette.ItemView.extend({
afterRender: function() {
// This will be called after rendering every inheriting view.
}
});
var SpecificItemView = MyApp.ItemView.extend({
// this view will automatically inherit the afterRender code.
});
In general, it seems to be considered good practice to define your own base views for all 3 view types. It will enable you to easily add global functionality later.
There is a common pattern used across all Backbone frameworks, normally they have a render method which in turn calls beforeRender, renderTemplate and afterRender methods.
render:function(){
this.beforeRender();
this.renderTemplate();// method names are just indicative
this.afterRender();
return this;
}
In your Base view you can have these methods to be empty functions, and implement them wherever you want it. Not sure this answer applies to Marionette
Combining thibaut's and Robert Levy's answer, the correct solution would be:
var baseView = Backbone.Marionette.ItemView.extend({
onDomRefresh: function() {
// This will be triggered after the view has been rendered, has been shown in the DOM via a Marionette.Region, and has been re-rendered
// if you want to manipulate the dom element of the view, access it via this.$el or this.$('#some-child-selector')
}
});
var SpecificItemView = baseView.extend({
// this view will automatically inherit the onDomRefresh code.
});

Can you bind a simple javascript array to your ember.js template?

I'm using ember.js RC1 + ember-data rev 11 (but I also need some plain ajax for configuration like models). I want to loop over a simple objects list and display the records (note -here I create just a basic array)
The content I have bound has the following custom find method defined
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
return this.records;
},
find: function() {
var self = this;
$.getJSON('/api/foo/', function(response) {
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
});
return this.records;
}
});
My other model uses this directly
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property()
});
Now inside my template
{{#each foo in related.listings}}
{{foo.name}}<br />
{{/each}}
The list loads up with whatever I put in the array by default (say I add a simple object using createRecord like so)
add: function(record) {
this.records.addObject(App.Foo.createRecord(record));
},
and when the template is rendered I see anything listed here... but as I put in the comments above, if I decide to remove records or null out the list that is bound it doesn't seem to reflect this in any way.
Is it possible to bind a simple array as I have and yet remove items from it using something basic such as splice? or even a drastic self.records = []; ?
self.records.splice(i, 1);
Even when I query the client manually after the splice or empty work it returns 0
console.log(App.Foo.all().get('length'));
Initially I see records, but then I see they are gone (yet the html doesn't change)
I understood your question this way, that the following remark is the point your are struggling with:
response.forEach(function(data) {
//say I want to kill everything in the array here for some strange reason...
self.records = [];
//the template still shows the record ... not an empty list ?
}, this);
You are wondering, why your template is showing no empty list? It's because you did not tell Ember when to update the template. You can tell Ember this way:
App.Related = DS.Model.extend({
listings: function() {
return App.Foo.find();
}.property("App.Foo.records.#each")
});
Now Ember knows, whenever something is added or removed from your array, it should update the listings property of your model. And therefore it knows that your view needs rerendering.
One additional remark to the orignal question regarding "simple javascript arrays". When you use Ember, you actually do not instantiate simple js arrays. When you declare:
var a = []; // is the same as -> var a = Ember.A();
Ember does some magic and wraps in an enhanced ember version of an array (Ember.NativeArray), which enables you to use such property dependency declarations mentioned above. This enables Ember to use ArrayObservers on those arrays, although they may feel like a plain JS Array.
You need to use the set method when you modify properties and get when you return them, or else Ember won't be able to do its magic and update the template.
In your case, there is an additional problem, which is that in find(), you return a reference to records before your asynchronous getJSON call replaces it with a new empty array. The calling method will never see the new array of records. You probably want to use clear() instead.
Your model should look something like this:
App.Foo = DS.Model.extend({
name: DS.attr('string')
}).reopenClass({
records: [],
all: function() {
// can't use 'this.get(...)' within a class method
return Ember.get(this, 'records');
},
findAll: function() {
var records = Ember.get(this, 'records');
$.getJSON('/api/foo/', function(response) {
records.clear();
// in this case my json has a 'foos' root
response.foos.forEach(function(json) {
this.add(json);
}, this);
}, this);
// this gets updated asynchronously
return records;
},
add: function(json) {
// in order to access the store within a
// class method, I cached it at App.store
var store = App.get('store');
store.load(App.Foo, json);
var records = Ember.get(this, 'records');
records.addObject(App.Foo.find(json.id));
}
});
Note that the addObject() method respects observers, so the template updates as expected. removeObject() is the corresponding binding-aware method to remove an element.
Here's a working jsfiddle.

Adding views and stores in controller dynamically - Ext-JS 4.1.1a

I want to add views and stores in controller dynamically. So, I've had this:
Ext.define('App.controller.MyController', {
extend: 'Ext.app.Controller',
stores: ['App.store.Users'],
views: ['App.view.Users.Index'],
I'm creating this controller dynamically with:
var controller = this.getController("Users");
How can I add store and views dynamically, something like:
var controller = this.getController(moduleID);
controller.stores = [];
controller.views = [];
controller.stores.push('App.store.Users');
controller.views.push('App.view.Users.Index');
But when I do that, it's not working. Console is telling me that Ext can't get "buffered from undefined" so I'm thinking that I have to do this with Ext.apply() or Ext.merge() or something like that to get getters and setters for stores.
What do you think?
EDIT for #asgoth:
When you use this.getController("nameOfController"); and if the controller doesn't exists, Ext-JS creates one for you. That's working 100% because when I console.log(controller); I'm getting data (and docs says that too). :)
You do not have that much choices, because you will need to have the arrays ready when you are instantiating the controller. By default this happens only once cause it should be managed by the Ext.app.Application Controller (instance).
First point is that you cannot use the getController method here because it does not accept any additional configuration. So the easiest solution would be the implementation of your own getController method, slightly renamed to avoid overriding.
here is a example one:
getControllerInstance: function(name, cfg) {
var me = this.application,
controllers = me.controllers,
controller = controllers.get(name);
if (!controller) {
controller = Ext.create(me.getModuleClassName(name, 'controller'), Ext.ApplyIf({
application: me,
id: name
},cfg);
controllers.add(controller);
if (me._initialized) {
controller.doInit(me);
}
}
return controller;
}
Please note that this variant does not add values to any array param instead it will override any any existing param!
Also note that all your controller will need to inherit from the controller that has this method.

how to render a collection using backbone?

I have this backbone that is working fine, i just want to render the collection is been fetched!
code:
var SearchView = Backbone.View.extend({
events: {
"keyup": "handleChange"
},
initialize: function(){
this.collection.bind("reset", this.updateView, this);
},
render: function(){
// this is where i need help
this.$el.next('#suggestions').append(// iterate over this.collection and apppend here) //
},
handleChange: function(){
var term = this.$el.val();
this.collection.search(term);
},
updateView: function() {
this.render();
}
});
I just want to iterate over this.collection and display the "text" attribute thats inside each collection and append it to the ('#suggestions') el. thanks
Focusing on your reset, render over collections issue, this is how I would do it. This way assumes that when you create your view, the $el is already present and you're passing it in through the View constructor so it's ready to go.
var SearchView = Backbone.View.extend({
template: _.template('<span><%= term %></span>');
initialize: function(){
this.collection.bind("reset", this.render, this);
},
render: function(){
this.addAllTerms();
return this;
},
addAllTerms: function() {
var self = this;
this.collection.each(function(model) {
self.addTerm(model);
});
},
addTerm: function(someModel) {
this.$el.next('#suggestions').append(this.template(someModel.toJSON()));
}
});
It's a bit different from your approach in a few ways. First, we utilize Underscore's template function. This could be anything from span to li to div whatever. We use the <%= %> to indicate that we're going to interpolate some value (which will come from our model.term attribute).
Instead of going to the handler then render, I just bind the collection to render.
The render() assumes we're always going to refresh the whole thing, build from scratch. addAllTerms() simply cycles through the collection. You can use forEach() or just each() which is the same thing except forEach() is 3 characters too long for my lazy bum. ;-)
Finally, the addTerm() function takes a model and uses it for the value that it will append to the #suggestions. Basically, we're saying "append the template with interpolated value". We defined the template function up above as this View object property to clearly separate the template from data. Although you could have skipped this part and just append('<span>' + someModel.get('term') + '</span>') or what not. The _.template() uses our template, and also takes any sort of object with the property that lines up with the one in our template. In this case, 'term'.
It's just a different way to do what you're doing. I think this code is a little more manageable. For example, maybe you want to add a new term without refreshing the whole collection. The addTerm() function can stand on its own.
EDIT:
Not that important but something I utilize with templates that I found useful and I didn't see it when I first started out. When you're passing the model.toJSON() into the template function, we're essentially passing all the model attributes in. So if the model is like this:
defaults: {
term: 'someTerm',
timestamp: '12345'
}
In our previous example, the attribute timestamp is also passed in. It isn't used, only <%= term %> is used. But we could easily interpolate it as well by adding it to the template. What I want to get at is that you don't have to limit yourself to data from one model. A complex template might have data from several models.
I don't know if it's the best way, but what I do is have something like this:
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
hash.category = anotherModel.get('category');
var date = new Date();
hash.dateAccessed = date.getTime();
return hash;
}
So you can easily build your own custom hash to throw into a template, aggregating all the data you want to interpolate into a single object to be passed into a template.
// Instead of .toJSON() we just pass in the `makeHash()` function that returns
// a customized data object
this.$el.next('#suggestions').append(this.template(this.makeHash()));
You can also easily pass in whole objects.
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
var animal = {
name: 'aardvark',
numLegs: 4
};
hash.animal = animal;
return hash;
}
And pass this into our template that looks like this:
template: _.template('<span><%= term %></span>
<span><%= animal.name %></span>
<span><%= animal.numLegs %></span>')
I'm not sure if this is the best way but it helped me understand exactly what data is going into my templates. Maybe obvious but it wasn't for me when I was starting out.
I found the solution to the problem, im gonna put it here for people that might want to know:
render: function(){
this.collection.forEach(function(item){
alert(item.get("text"));
});
}

Categories