Understanding Backbone architecture base concepts - backbone.js

I'm trying to working with backbone but I'm missing it's base concepts because this is the first JavaScript MVVM Framework I try.
I've taken a look to some guide but I think I still missing how it should be used.
I'll show my app to get some direction:
// Search.js
var Search = {
Models: {},
Collections: {},
Views: {},
Templates:{}
};
Search.Models.Product = Backbone.Model.extend({
defaults: search.product.defaults || {},
toUrl:function (url) {
// an example method
return url.replace(" ", "-").toLowerCase();
},
initialize:function () {
console.log("initialize Search.Models.Product");
}
});
Search.Views.Product = Backbone.View.extend({
initialize:function () {
console.log("initialize Search.Views.Product");
},
render:function (response) {
console.log("render Search.Views.Product");
console.log(this.model.toJSON());
// do default behavior here
}
});
Search.Models.Manufacturer = Backbone.Model.etc...
Search.Views.Manufacturer = Backbone.View.etc...
then in my web application view:
<head>
<script src="js/jquery.min.js"></script>
<script src="js/underscore.min.js"></script>
<script src="js/backbone/backbone.min.js"></script>
<script src="js/backbone/Search.js"></script>
</head>
<body>
<script>
var search = {};
search.product = {};
search.product.defaults = {
id:0,
container:"#search-results",
type:"product",
text:"<?php echo __('No result');?>",
image:"<?php echo $this->webroot;?>files/product/default.png"
};
$(function(){
var ProductModel = new Search.Models.Product();
var ProductView = new Search.Views.Product({
model:ProductModel,
template:$("#results-product-template"),
render:function (response) {
// do specific view behavior here if needed
console.log('render ProductView override Search.Views.Product');
}
});
function onServerResponse (ajax_data) {
// let's assume there is some callback set for onServerResponse method
ProductView.render(ajax_data);
}
});
</script>
</body>
I think I missing how Backbone new instances are intended to be used for, I thought with Backbone Search.js I should build the base app like Search.Views.Product and extend it in the view due to the situation with ProductView.
So in my example, with render method, use it with a default behavior in the Search.js and with specific behavior in my html view.
After some try, it seems ProductModel and ProductView are just instances and you have to do all the code in the Search.js without creating specific behaviors.
I understand doing it in this way make everything easiest to be kept up to date, but what if I use this app in different views and relative places?
I'm sure I'm missing the way it should be used.
In this guides there is no code used inside the html view, so should I write all the code in the app without insert specific situations?
If not, how I should write the code for specific situations of the html view?
Is it permitted to override methods of my Backbone application?

Basically, you should think of the different parts like this:
templates indicate what should be displayed and where. They are writtent in HTML
views dictate how the display should react to changes in the environment (user clicks, data changing). They are written in javascript
models and collections hold the data and make it easier to work with. For example, if a model is displayed in a view, you can tell the view to refresh when the model's data changes
then, you have javascript code that will create new instances of views with the proper model/collection and display them in the browser
I'm writing a book on Marionette.js, which is a framework to make working with Backbone easier. The first chapters are available in a free sample, and explain the above points in more detail: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf

Related

backbonejs demo case can't work well with localstorage added in stack snippet

I am trying to use the stack snippet tool to embed live demo case into my post. But I find when I add localstorage function into the demo case, it can't work well.
So I simplified my question to the basic backbone case, to emphasis the localstorage issue as above.
And if I remove the localstorage flow, the demo can run through very well, but if localstorage added, then it can't work well. The error message from console said
Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
Any idea?
// A simple case
var Daymodel = Backbone.Model.extend({
defaults : {
day: 1,
}
});
var DayCollection = Backbone.Collection.extend({
model: Daymodel,
localStorage: new Backbone.LocalStorage("test-simple")
});
// The view for each day panel
var DayView = Backbone.View.extend({
tagName:"div",
template: _.template( $('#eachday-template').html() ),
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
// The view for the entire application
var AppView = Backbone.View.extend({
el: $('#todoapp'),
events: {
"click #add-firebase":"addToLocalhost"
},
initialize: function() {
this.daylist = this.$("#container"); // the daylist to append to
this.listenTo(this.collection, 'add', this.addOne);
this.collection.fetch();
},
addOne: function(todo) {
var view = new DayView({model:todo});
this.daylist.append(view.render().el);
},
addToLocalhost: function(){
this.collection.create({
day : this.collection.length + 1,
});
}
});
// Create a function to kick off our BackboneFire app
function init() {
// The data we are syncing from our remote Firebase database
var collection = new DayCollection();
var app = new AppView({ collection: collection });
}
// When the document is ready, call the init function
$(function() {
init();
});
<div id="todoapp">
<div id="container"></div>
<button id="add-firebase">Add to Localstorage</button>
</div>
<script type="text/template" id="eachday-template">
<h3 class="which-day"> day <%= day %></h3>
<ul id="todo-list"></ul>
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.1.16/backbone.localStorage-min.js">
</script>
The answer is in the error message you're getting: "The document is sandboxed". You can't mess with the localStorage as it's a restricted feature for all sandboxed iframe documents unless the restriction is specifically lifted.
If you look at the page source, you'll see the iframe and the sandbox attribute options.
<iframe name="d62428c9-4eba-3156-6ef7-8815d959a281"
sandbox="allow-modals allow-scripts"
class="snippet-box-edit" frameborder="0">
See Play safely in sandboxed IFrames for more details.
The framed document is loaded into a unique origin, which means that
all same-origin checks will fail; unique origins match no other
origins ever, not even themselves. Among other impacts, this means
that the document has no access to data stored in any origin's cookies
or any other storage mechanisms (DOM storage, Indexed DB, etc.).
[...]
With the exception of plugins, each of these restrictions can be
lifted by adding a flag to the sandbox attribute’s value. Sandboxed
documents can never run plugins, as plugins are unsandboxed native
code, but everything else is fair game:
allow-forms allows form submission.
allow-popups allows popups (window.open(), showModalDialog(), target="_blank", etc.).
allow-pointer-lock allows (surprise!) pointer lock.
allow-same-origin allows the document to maintain its origin; pages loaded from https://example.com/ will retain access to that
origin's data.
allow-scripts allows JavaScript execution, and also allows features to trigger automatically (as they'd be trivial to implement
via JavaScript).
allow-top-navigation allows the document to break out of the frame by navigating the top-level window.
For allow-modals, Add allow-modals to the sandbox of Stack Snippets gives more details:
Chrome blocks modal dialogs such as alert, confirm and prompt in
sandboxed iframes unless allow-modals is set. This behavior became the
default as of Chrome 46 and Opera 34.

Backbonejs: model not getting passed in underscore template

I am new to backbonejs. What I am trying to do is, render a template on page load and pass model as data parameter in _.template function. Here is my bacbone code:
var Trip = Backbone.Model.extend({
url: '/trips/' + trip_id + '/show'
});
var InviteTraveller = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var trip = new Trip();
trip.fetch({
success: function(){
console.log(trip); //logs trip object correctly
var template = _.template($('#invite-traveller-template').html(), {trip: trip});
that.$el.html(template);
}
});
}
});
var Router = Backbone.Router.extend({
routes: {
'': 'fetchTrip'
}
});
var inviteTraveller = new InviteTraveller();
var router = new Router();
router.on('route:fetchTrip',function () {
inviteTraveller.render();
});
Backbone.history.start();
And here is my sample template:
<script type="text/template" id="invite-traveller-template">
<h3>Trip</h3>
<h3><%= trip.get('name') %></h3>
</script>
On running, I am getting the this in browser window and console shows:
trip is not defined
I am facing this issue since yesterday but could not figure out the solution yet. Not understanding what is going wrong, code also seems to be right. Any help would be greatly appreciated.
Update:
I removed
inviteTravellers.render();
from router.on() and then reloaded the page in browser. I still got same error which means that <script></script> (template) is being compiled before calling render() of InviteTraveller view. What can be the possible reason for this?
I had the same issue (underscore v1.8.2). My fix:
var template = _.template($('#invite-traveller-template').html());
var compiled = template({trip: trip});
that.$el.html(compiled);
You're passing the whole model to the template. Typically you would call model.toJSON and then pass its result to the template. Additionally using <%= in your template to render the attribute, which is meant for interpolating variables from that JSON object you're passing.
You can pass a whole model to the template and use <% ... %> to execute pure Javascript code and use print to get the attribute but it's probably overkill.
Have a look at this fiddle.
You code work perfectfly, here's it
I think that your problem came from another code, not the one you have posted, because there's no way for your view to render if you remove :
inviteTravellers.render();
Try to chaneg <h3><% trip.get('name'); %></h3> by <h3><%= trip.get('name') %></h3>
My code seems to be right but still my template was getting compiled on page load and I was getting trip is not defined error. I did not understand the reason of this behavior yet.
I solved this issue by using handlebarsjs instead of default underscore templates.

Singleton model in Backbone multipage app with RequireJS

I have a Backbone multipage app written with the use of RequireJS. Since it's multipage I decided not to use a router as it got too messy. I've tried multiple ways of creating a singleton object to be used throughout the app
var singletonModel= Backbone.Model.extend({
}),
return new singletonModel;
For the above I'm just referencing the singletonModel model in my class using the define method and then calling it as is
this.singleton = singletonModel;
this.singleton.set({'test': 'test'});
On a module on my next page when I then call something similar to
this.singleton = singletonModel;
var test = this.singleton.get('test');
The singleton object seems to get re-initialized and the test object is null
var singletonModel= Backbone.Model.extend({
}, {
singleton: null,
getSingletonModelInst: function () {
singletonModel.singleton =
singletonModel.singleton || new singletonModel;
return singletonModel.singleton;
}
});
return singletonModel;
For the above I'm just referencing the singletonModel model in my class using the define method and then calling it as is
this.singleton = singletonModel.getSingletonModelInst();
this.singleton.set({'test': 'test'});
On a module on my next page when I then call something similar to
this.singleton = singletonModel.getSingletonModelInst();
var test = this.singleton.get('test');
Again it looks like the singleton object is getting re-initialized and the test object is null.
I'm wondering if the issue is because I'm using a multi-page app with no router so state is not been preserved? Has anyone tried using a singleton object in a multi-page app before? If so did you do anything different to how it's implemented on a single-page app?
Thanks,
Derm
Bart's answer is very good, but what it's not saying is how to create a singleton using require.js. The answer is short, simply return an object already instanciated :
define([], function() {
var singleton = function() {
// will be called only once
}
return new singleton()
})
Here we don't have a singleton anymore :
define([], function() {
var klass = function() {
// will be called every time the module is required
}
return klass
})
It's may sound a little ... but, you doing a multi-page application, so when you move to next page, a whole new document was loaded into the browser, and every javascript on it will be loaded too, include require.js and your model. so the require.js was reloaded, and it create your model again, so you got a different model than you thought.
If above was true, my opinion is your model will "live" on a single page, when you jump to then next page, that model was "kill"ed by browser. so If you want see it again, store it on somewhere else, maybe server or localstroe, on the former page. and in the next page load it back from server or localstore, and wrap it into a Backbone model, make it "live" again.
Here is how I implemented a singleton in a recent Backbone/Require application. State is remembered across any number of views.
instances/repoModel.js
define(['models/Repo'],
function(RepoModel){
var repoModel = new RepoModel();
return repoModel;
}
);
models/Repo.js
define(['backbone'],
function(Backbone){
return Backbone.Model.extend({
idAttribute: 'repo_id'
});
}
);
views/SomePage.js
define(['backbone', 'instances/repoModel'],
function(Backbone, repoModel) {
return Backbone.View.extend({
initialize: function() {
repoModel.set('name', 'New Name');
}
});
}
);

Need help understanding the basics of nested views in backbone

I've been doing a bunch of reading about nested views in backbone.js and I understand a good amount of it, but one thing that is still puzzling me is this...
If my application has a shell view that contains sub-views like page navigation, a footer, etc. that don't change in the course of using the application, do I need to render the shell for every route or do I do some kind of checking in the view to see if it already exists?
It would seem so to me if someone didn't hit the "home" route before moving forward in the app.
I haven't found anything helpful about this in my googling, so any advice is appreciated.
Thanks!
Since your "shell" or "layout" view never changes, you should render it upon application startup (before triggering any routes), and render further views into the layout view.
Let's say your layout looked something like this:
<body>
<section id="layout">
<section id="header"></section>
<section id="container"></section>
<section id="footer"></section>
</section>
</body>
Your layout view might look something like this:
var LayoutView = Backbone.View.extend({
el:"#layout",
render: function() {
this.$("#header").html((this.header = new HeaderView()).render().el);
this.$("#footer").html((this.footer = new FooterView()).render().el);
return this;
},
renderChild: function(view) {
if(this.child)
this.child.remove();
this.$("#container").html((this.child = view).render().el);
}
});
You would then setup the layout upon application startup:
var layout = new LayoutView().render();
var router = new AppRouter({layout:layout});
Backbone.history.start();
And in your router code:
var AppRouter = Backbone.Router.extend({
initialize: function(options) {
this.layout = options.layout;
},
home: function() {
this.layout.renderChild(new HomeView());
},
other: function() {
this.layout.renderChild(new OtherView());
}
});
There are a number of ways to skin this particular cat, but this is the way I usually handle it. This gives you a single point of control (renderChild) for rendering your "top-level" views, and ensures the the previous element is removed before new one is rendered. This might also come in handy if you ever need to change the way views are rendered.

using underscore variables with Backbone Boilerplate fetchTemplate function

I am building an application using the Backbone Boilerplate, and am having some trouble getting underscore template variables to work. I have a resource named Goal. My Goal View's render function looks like this:
render: function(done) {
var view = this;
namespace.fetchTemplate(this.template, function(tmpl) {
view.el.innerHTML = tmpl();
done(view.el);
});
}
I'm calling it inside of another view, like so:
var Goal = namespace.module("goal");
App.View = Backbone.View.extend({
addGoal: function(done) {
var view = new Goal.Views.GoalList({model: Goal.Model});
view.render(function(el) {
$('#goal-list').append(el);
});
}
});
I'm using local storage to save my data, and it's being added just fine. I can see it in the browser, but for some reason, when I load up the app, and try to fetch existing data, i get this error:
ReferenceError: Can't find variable: title
Where title is the only key I'm storing. It is a direct result of calling:
tmpl();
Any thoughts are greatly appreciated.
Your template is looking for a variable title, probably like this <%- title %>. You need to pass it an object like this tmpl({ title: 'Some title' })
Turns out, I wasn't passing in the model when i created the view, which was making it impossible to get the models data. Once I passed in the model correctly, I could then pass the data to tmpl, as correctly stated by #abraham.
render: function(done) {
var
view = this,
data = this.model.toJSON();
clam.fetchTemplate(this.template, function(tmpl) {
view.el.innerHTML = tmpl(data);
done(view.el);
});
},

Resources