How to reference a Backbone/Marionette View within itself? - javascript

MyView.js:
define(['app/models/MyModel'],
function (MyModel) {
return Mn.LayoutView.extend({
template: '#my-template',
className: 'my-classname',
regions: {
content: '.content-region',
panel: '.panel-region'
}
initialize: function () {
_.bindAll(this, 'childButtonClicked');
},
onShow: function () {
this.getRegion('content').show(new AnotherView());
},
childEvents: {
'some-child-click': 'childButtonClicked'
},
childButtonClicked: function (view) {
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}
});
});
I'm trying to nest instances of MyView within itself. This worked correctly when I was building the prototype by dumping everything into one function, like so:
var MyView = Mn.LayoutView.extend({
...
childButtonClicked: function(view) {
var newView = new MyView({
...
Now that I'm trying to separate the Views into their own files and use require.js, I can't figure out the syntax for a self-referential view.
When I run this code as is, I get an error like 'MyView is undefined'.
If I add it to the require header like so:
define(['app/models/MyModel', 'app/views/MyView'],
function (MyModel, MyView) {
I get the error 'MyView is not a function'.
EDIT for solution:
The marked solution works fine, I ended up using the obvious-in-hindslght:
define(['app/models/MyModel'],
function (MyModel) {
var MyView = Mn.LayoutView.extend({
template: '#my-template',
className: 'my-classname',
regions: {
content: '.content-region',
panel: '.panel-region'
}
initialize: function () {
_.bindAll(this, 'childButtonClicked');
},
onShow: function () {
this.getRegion('content').show(new AnotherView());
},
childEvents: {
'some-child-click': 'childButtonClicked'
},
childButtonClicked: function (view) {
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}
});
return MyView;
});

You can require() in your module: var MyView = require(app/views/MyView);.
So for want of a better place:
childButtonClicked: function (view) {
var MyView = require(app/views/MyView);
var newView = new MyView({
model: new MyModel({
title: view.model.get('title')
})
});
this.getRegion('panel').show(newView);
}

Related

Backbone create a new view instance every request page

In my App i have created a View. this View is composed of a Template like a little Form. The Form has an button and in my View i create an click event to handle this button to create a new instance of another View passing the Form data to this View and put the data on html element. The problem is: if i enter in home route or in product 3 times and send a Form data, will appears 3 same Form datas.
Form view
window.userFormView = Backbone.View.extend({
el:$("#principal"),
events : {
'click .userButton' : 'newUser'
},
initialize:function(){
this.template = _.template($("#userFormView").html());
},
newUser : function(ev) {
ev.preventDefault();
//criamos uma nova instancia do model
window.user_view = new userViewes({model: users});
var u = { nome : $("#iName").val() ,sobrenome : $("#iLName").val() };
var user = new userModel(u);
users.add(user);
console.log(users);
return false;
},
render: function() {
this.$el.html("");
this.$el.html(this.template);
}
});
Form Template View
<script type="text/template" id="userFormView">
<form action="" id="form-new-user" class="formulario">
<span class="label">Name?</span><input type="text" id="iName" class="input">
<span class="label">Last Name?</span><input type="text" id="iLName" class="input">
<button class="userButton">Send</button>
<hr>
</form>
</script>
and my route are like this:
window.AppRouter = Backbone.Router.extend({
//
// Definindo rotas
//
routes: {
'home': 'index',
'product': 'productsList',
'foo1': 'doNothing1',
'foo2': 'doNothing2'
},
index: function () {
window.users = new userCollections();
window.userForm = new userFormView();
},
productsList : function() {
window.pCollection = new productCollections();
window.produtoForm = new produtoFormView();
},
doNothing1: function () {
console.log('doNothing1()');
},
doNothing2: function () {
console.log('doNothing2()');
}
});
window.router = new AppRouter();
Backbone.history.start();
userViewes view
window.userViewes = Backbone.View.extend({
// model: users,
el: $("#userContainer"),
initialize: function(){
this.model.on("add", this.render, this);
this.model.on("remove", this.render, this);
},
render: function() {
var self = this;
self.$el.html("");
this.model.each(function(user, indice) {
self.$el.append((new userView({model: user })).render().$el);
});
return this;
}
});
and finally userView:
window.userView = Backbone.View.extend({
//model: new userModel(),
tagName : 'div',
class : "userName",
events :{
'click .editar' : 'editar',
'click .remover' : 'remover',
'blur .sobrenome': 'fechar',
'keypress .sobrenome' : 'onEnterUpdate',
},
editar : function(ev) {
ev.preventDefault();
this.$('.sobrenome').attr('contenteditable', true).focus();
},
fechar : function(ev) {
var sobrenome = $(".sobrenome").text();
this.model.set("sobrenome", sobrenome);
$(".sobrenome").val();
this.$(".sobrenome").removeAttr("contenteditable");
},
onEnterUpdate : function(ev) {
var self = this;
if(ev.keyCode == 13) {
self.fechar();
_.delay(function(){
self.$(".sobrenome").blur();
}, 100);
}
},
remover : function(ev) {
ev.preventDefault();
window.users.remove(this.model);
},
initialize: function(){
this.template = _.template($("#userTemplate").html());
},
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
When your view is using el option, make sure you clean up the existing view before you make a new one.
As it is, every time you switch between routes (without a full page refresh) a new instance pointing to same element is created which causes more and more event handlers to be bound to the el element which is in DOM, and the views stay in memory because of the binding. Try something like:
index: function () {
window.users = window.users || new userCollections();
if(window.userForm){
// clean up is important
window.userForm.remove();
}
window.userForm = new userFormView();
},
And of course, instead of repeating similar code in all routes, have a variable like this.currentView that points to the active view, and a common function that does necessary clean up
P.S: Adding properties to window object is a bad practice. Create your own name space or use the Router instance instead of window
I have found the answer. i implemented singleton pattern to get only one instance of the object. follow the code:
var single = (function(){
function createInstance() {
window.userForm = new userFormView();
window.users = new userCollections();
}
function users() {
return window.users;
}
function userForm() {
return window.userForm;
}
return {
init : function() {
if(!window.users && !window.userForm) {
createInstance();
}else{
this.render();
}
},
render: function() {
window.userForm.render();
}
}
}());
single.init();

id is not defined at child.eval

I'm writing my first Backbone blog app but when i try to add new post it throws an error.
Here is my app.js (all of backbone related components are in this file):
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var Post = Backbone.Model.extend({});
var Posts = Backbone.Collection.extend({
model : Post,
url : "/posts"
});
var PostListView = Backbone.View.extend({
tagName: "li",
template: _.template("<a href='/posts/{{id}}'>{{title}}</a>"),
events: {
'click a': 'handleClick'
},
handleClick: function (e) {
e.preventDefault();
postRouter.navigate($(e.currentTarget).attr("href"),
{trigger: true});
},
render: function () {
this.el.innerHTML = this.template(this.model.toJSON());
return this;
}
});
var PostsListView = Backbone.View.extend({
template: _.template("<h1>My Blog</h1><a href='/post/new' class='newPost'>New</a> <ul></ul>"),
events: {
'click .newPost': 'handleNewClick'
},
handleNewClick: function (e) {
e.preventDefault();
postRouter.navigate($(e.currentTarget).attr("href"),
{trigger: true});
},
render: function () {
this.el.innerHTML = this.template();
var ul = this.$el.find("ul");
this.collection.forEach(function (post) {
ul.append(new PostListView({
model: post
}).render().el);
});
return this;
}
});
var PostView = Backbone.View.extend({
template: _.template($("#postView").html()),
events: {
'click a': 'handleClick'
},
render: function () {
var model = this.model.toJSON();
model.pubDate = new Date(Date.parse(model.pubDate)).toDateString();
this.el.innerHTML = this.template(model);
return this;
},
handleClick: function (e) {
e.preventDefault();
postRouter.navigate($(e.currentTarget).attr("href"),
{trigger: true});
return false;
}
});
var PostFormView = Backbone.View.extend({
tagName: 'form',
template: _.template($("#postFormView").html()),
initialize: function (options) {
this.posts = options.posts;
},
events: {
'click #submitPost': 'createPost',
'click .back' : 'backButton'
},
render: function () {
this.el.innerHTML = this.template();
return this;
},
backButton: function (e) {
e.preventDefault();
postRouter.navigate($(e.currentTarget).attr("href"),
{trigger: true});
return false;
},
createPost: function (e) {
e.preventDefault();
var postAttrs = {
content: $("#postText").val(),
title: $("#postTitle").val(),
pubDate: new Date(),
};
this.posts.create(postAttrs);
postRouter.navigate("/", { trigger: true });
return false;
}
});
var PostRouter = Backbone.Router.extend({
initialize: function (options) {
this.posts = options.posts;
this.main = options.main;
},
routes: {
'': 'index',
'posts/:id': 'singlePost',
'post/new': 'newPost'
},
index: function () {
var pv = new PostsListView({ collection: this.posts });
this.main.html(pv.render().el);
},
singlePost: function (id) {
var post = this.posts.get(id);
var pv = new PostView({ model: post });
this.main.html(pv.render().el);
},
newPost: function () {
var pfv = new PostFormView({ posts: this.posts });
this.main.html(pfv.render().el);
}
});
I also have some view templates in my index file :
<!DOCTYPE html>
<html>
<head>
<title> Simple Blog </title>
</head>
<body>
<div id="main"></div>
<script src="/jquery.js"></script>
<script src="/underscore.js"></script>
<script src="/backbone.js"></script>
<script type="text/template" id="postFormView">
All Posts<br />
<input type="text" id="postTitle" placeholder="post title" />
<br />
<textarea id="postText"></textarea>
<br />
<button id="submitPost"> Post </button>
</script>
<script type="text/template" id="postView">
<a href='/'>All Posts</a>
<h1>{{title}}</h1>
<p>{{pubDate}}</p>
{{content}}
</script>
<script src="/app.js"></script>
<script>
var postRouter = new PostRouter({
posts: new Posts(<%- posts %>),
main: $("#main")
});
Backbone.history.start({pushState: true});
</script>
</body>
</html>
Viewing posts and home page works fine but when I try to create a new post I get this error from the dev tools console:
Uncaught ReferenceError: id is not defined
at child.eval (eval at _.template (http://localhost:3000/underscore.js:1:1), <anonymous>:6:8)
at child.template (http://localhost:3000/underscore.js:1214:21)
at child.render (http://localhost:3000/app.js:27:34)
at http://localhost:3000/app.js:48:16
at Array.forEach (native)
at Function._.each._.forEach (http://localhost:3000/underscore.js:79:11)
at child.Collection.(anonymous function) [as forEach] (http://localhost:3000/backbone.js:956:24)
at child.render (http://localhost:3000/app.js:45:25)
at child.index (http://localhost:3000/app.js:118:27)
at Object.callback (http://localhost:3000/backbone.js:1242:30)
The server is a simple nodejs server and output for creating a post is something like this:
{"result":{"ok":1,"n":1},"ops":[{"content":"kljhlkjh","title":"jkhjklh","pubDate":"2016-10-29T10:21:47.793Z","id":12,"_id":"5814783b732bbe153461eca4"}],"insertedCount":1,"insertedId
s":["5814783b732bbe153461eca4"]}
Where is the error?
First, you need to find what is causing the error, and where does it comes from.
The error comes from the following line inside the PostListView's render function:
this.el.innerHTML = this.template(this.model.toJSON());
And the error is thrown by underscore's template rendering. Which comes down to:
template: _.template("<a href='/posts/{{id}}'>{{title}}</a>"),
See the {{id}}? It's that one that is not defined when the error occurs.
What "{{id}} not defined" means?
You're passing this.model.toJSON() as the data for the template. So, it means that this.model's id attribute is not defined yet.
Why is my model id not defined yet?
It's because creating a new model through the collection's create function is asynchronous.
this.posts.create(postAttrs);
// the model hasn't received an id from the server yet.
postRouter.navigate("/", { trigger: true });
How to wait after a collection's create call?
Backbone offers success and error callbacks for most (if not all) its asynchronous functions.
The options hash takes success and error callbacks which will both be
passed (collection, response, options) as arguments.
So, you could change your createPost function to the following, adding a onPostCreated callback.
createPost: function(e) {
e.preventDefault();
var postAttrs = {
content: $("#postText").val(),
title: $("#postTitle").val(),
pubDate: new Date(),
};
this.posts.create(postAttrs, {
context: this,
success: this.onPostCreated
});
return false;
},
onPostCreated: function() {
// you don't need to use the router, so your views are self-contained
Backbone.history.navigate("/", { trigger: true });
},

application.rootView.getRegion(...).show is not a function

I have a root layout with two regions. One of them is simple ItemView, but another is LayoutView. When the second region try to show a LayoutView i receive an error
application.rootView.getRegion(...).show is not a function
in console. Can anyone help me to understand why this happening?
Views:
_RootView = Marionette.LayoutView.extend({
el: 'body',
regions: {
navigationRegion: {
selector: '.section-navigation',
regionClass: _NavigationRegion
},
contentLayout: {
selector: '.section-content',
regionClass: _ContentLayout
}
}
});
_ContentLayout = Marionette.LayoutView.extend({
template: '#content-template',
regions: {
contentRegion: {
selector: '.content',
regionClass: _ContentRegion
},
pagemasterRegion: {
selector: '.pagemaster',
regionClass: _PagemasterRegion
}
}
});
Application:
application = new _Application({
initialize: function (options) {
this.rootView = new _RootView();
}
});
application.on('start', function () {
var
contentLayout = new _ContentLayout();
var
navigation = new _Navigation();
navigation.fetch({
success: function (collection, response, options) {
var
navigationView = new _NavigationView({
collection: collection
});
//======Line without error=======
application.rootView
.getRegion('navigationRegion')
.show(navigationView);
}
});
//======Line with error=======
application.rootView
.getRegion('contentLayout')
.show(contentLayout);
});
application.start();
You should render layout before showing it:
application.rootView.render();
application.rootView
.getRegion('contentLayout')
.show(contentLayout);

Backbone.js master-detail view, navigation issue (jsfiddle included)

I'm struggling to get a simple master-detail scenario working with Backbone. Here's the jsfiddle and code is below.
Problem 1: this navigation doesn't work at all if I switch "pushstate" to true. What I really want is to have no hashes/pound signs in my urls.
Problem 2: my users might rock up on a url like /accommodation/287, not always on the home page. How would you deal with that using the router?
Thanks a lot for any help!
var AccommodationItem = Backbone.Model.extend({
defaults: {
html: "",
loaded: false
},
urlRoot: "/Home/Accommodation/"
});
var AccommodationItemView = Backbone.View.extend({
tagName: "li",
template: _.template("<a href='#accommodation/<%= id %>'><%= description %></a>"),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var AccommodationList = Backbone.Collection.extend({
model: AccommodationItem
});
var DetailView = Backbone.View.extend({
initialize: function () { },
render: function () {
this.$el.html(this.model.get("html"));
},
setModel: function (model) {
this.model = model;
var $this = this;
if (!this.model.get("loaded")) {
/*
this.model.fetch({ success: function () {
$this.model.set("loaded", true);
$this.render();
}
});*/
$this.model.set("html", "<h2>Full item " + this.model.get("id") + "</h2>");
$this.model.set("loaded", true);
$this.render();
} else {
$this.render();
}
}
});
var AccommodationListView = Backbone.View.extend({
tagName: "ul",
initialize: function () {
this.collection.on("reset", this.render, this);
},
render: function () {
this.addAll();
},
addOne: function (item) {
var itemView = new AccommodationItemView({ model: item });
this.$el.append(itemView.render().el);
},
addAll: function () {
this.collection.forEach(this.addOne, this);
}
});
var App = new (Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
initialize: function () {
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
},
start: function () {
Backbone.history.start({ pushState: false });
},
index: function () {
this.fetchCollections();
},
show: function (id) {
var model = this.accommodationList.get(id);
this.detailView.setModel(model);
},
fetchCollections: function () {
var items = [{ id: 1, description: "item one" }, { id: 2, description: "item two" }, { id: 3, description: "item three" }];
this.accommodationList.reset(items);
}
}));
$(function () {
App.start();
});
EDIT: In a comment below I mentioned the Codeschool backbone.js tutorial. Just want to say that I have now finished BOTH parts of the course and it DOES cover exactly the AppView pattern described in the accepted answer. It's an excellent course and I thoroughly recommend it.
you have a few of the concepts mixed up.
There is too much to explain here, so I've (very roughly) put together a patch of your code that works as you intend. I would advise that you put it side-by-side with your own and see what I have done differently.
http://jsfiddle.net/wtxK8/2
A couple of things, you should not init Backbone.history from within a router. your 'init' should look something more like this
$(function () {
window.app = new App();
window.appView = new AppView({el:document});
Backbone.history.start({ pushState: true });
});
This is setting a 'wrapper' view than encompasses the entire page. Also, you have far too much logic in your router. Try to only use the router for routes. After my quick re factor, your router only contains this:
var App = Backbone.Router.extend({
routes: {
"": "index",
"accommodation/:id": "show"
},
show: function (id) {
var model = window.appView.accommodationList.get(id);
window.appView.detailView.setModel(model);
}
});
The AppView (that I have written for you now does all of that initialize work.
var AppView = Backbone.View.extend({
initialize : function(){
this.detailView = new DetailView({ model: new AccommodationItem({ id: 1 }) });
$("#detail").append(this.detailView.el);
this.accommodationList = new AccommodationList();
this.accommodationListView = new AccommodationListView({ collection: this.accommodationList });
$("#app").append(this.accommodationListView.el);
this.fetchCollections();
},
fetchCollections: function () {
var items = [
{ id: 1, description: "item one" },
{ id: 2, description: "item two" },
{ id: 3, description: "item three" }
];
this.accommodationList.reset(items);
}
});
Even after my re factor, it's still far from optimal, but I have provided it all to help you on your journey of learning :)
I would then recommend you follow some of the on-line tutorials step-by-step so that you can set up the structure of your app in a better way.
Good Luck, and be sure to check out http://jsfiddle.net/wtxK8/2 to see it working.
EDIT: I have not address your second question. there is enough to be worked on with question 1 to keep you busy. If I have more time later, I will help further.

Backbone.js Collection not iterating

PaperSection = Backbone.Model.extend({
defaults: {
title: '',
position: ''
},
initialize: function(){
},
renderView: function(){
return "<li>"+this.get('title')+", Position: "+this.get('position')+"</li>"
}
});
PaperSectionsList = Backbone.Collection.extend({
url: '/admin/paper/section/list.html',
size: 6,
initialize: function(){
this.add(new PaperSection({
id:1,
title: "Hello World",
position:1
}));
},
comparator: function(section){
return section.get('position');
},
renderView: function(){
var html = "<ul>";
_.each(this.models, function(section){
html += section.renderView();
});
if(_.size(this.models) < this.size){
html+="<li><a href='#add_section' class='btn btn-success btn-small' id='add_section'>Add Section</a></li>"
}
html+="</ul>";
return html;
}
});
PaperSectionView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
console.log(this.collection.get(1));
var html = this.collection.renderView();
this.$el.html(html);
}
});
var paper_sections = new PaperSectionsList({
model: PaperSection,
});
var section_view = new PaperSectionView({
collection: paper_sections,
el: $('#paper_sections')
});
When I run the code I get the error that section.renderView() is not a function. Need help with this. How do I iterate over models in my collection?
Your first problem is that you are defining your collection and instantiating it incorrectly.
The model declaration needs to happen in the collection's definition, not in the instantiation:
PaperSectionsList = Backbone.Collection.extend({
model: PaperSection,
And then you just instantiate it:
var paper_sections = new PaperSectionsList();
That will get your code working.
But, I feel compelled to point out that you have some confusion about your coding concerns. Models and Collections should never have functions called render*. These are View concerns. In your case, the idiomatic way of handling it would be to have to views: PaperSectionListView (ul) and PaperSectionListItem (li). The templates and render functions live in those views.
I've got your code working as follows...
But I think the above answer handles the core issue, and I agree with the suggestions that the Models and Collections should not be handling render logic.
Note: I cleaned up some JSLint errors such as and extra comma and missing semicolons.
var PaperSection = Backbone.Model.extend({
defaults: {
title: '',
position: ''
},
initialize: function () {
},
renderView: function () {
return "<li>" + this.get('title') + ", Position: " + this.get('position') + "</li>";
}
});
var PaperSectionsList = Backbone.Collection.extend({
url: '/admin/paper/section/list.html',
model: PaperSection,
size: 6,
initialize: function () {
this.add(new PaperSection({
id: 1,
title: "Hello World",
position: 1
}));
},
comparator: function (section) {
return section.get('position');
},
renderView: function () {
var html = "<ul>";
_.each(this.models, function (section) {
html += section.renderView();
});
if (_.size(this.models) < this.size) {
html += "<li><a href='#add_section' class='btn btn-success btn-small' id='add_section'>Add Section</a></li>";
}
html += "</ul>";
return html;
}
});
var PaperSectionView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
console.log(this.collection.get(1));
var html = this.collection.renderView();
this.$el.html(html);
}
});
$(function () {
var paper_sections = new PaperSectionsList({
model: PaperSection
});
var section_view = new PaperSectionView({
collection: paper_sections,
el: $('#paper_sections')
});
});

Categories

Resources