I hope i am clear enough, otherwise ask me for clarifications.
I would like to delete end create view in agreement to a main template…
I did try the following implementation, unsuccessfully.
// main-template.html
<div id=""project>
<div class="main-template">
<div id="view1"></div>
<div class="foldable-block">
<div id="view2"></div>
<div id="view3"></div>
</div>
</div>
</div>
//mainView.js
define([
"js/views/view1",
"js/views/view2",
"js/views/view3",
"text!templates/main-template.html"
], function (View1, View2, View3, mainTemaplte) {
var MainView = Backbone.View.extend({
initialize: function ()
{
this.$el.html(Mustache.render(mainTemaplte));
this.render();
},
el: $("#project"),
render: function ()
{
var options = this.options;
this.view1 = new View1(options);
this.view2 = new View2(options);
this.view3 = new View3(options);
}
});
return MainView;
});
//view1.js
define([… ], function (..) {
var View1 = Backbone.View.extend({
el: $("#view1"),
initialize: function () {
console.log(this.$el) // []
setTimeout( function () {
console.log(this.$el) // []
}, 2000);
}
});
return View1;
});
The issues as you can see from the comments is in view1.js
(this.$el) // []
from my js console it works:
$("#view1") // <div>….</div>
My goals is:
1) when I load the mainView.js module I would like to create a template to which attach my views (view1, view2, view3)
2) when I trigger delete view, every DOM, to which are attached the view, should be deleted.
3) when I call again the mainView.js module the template should be recreated.
if you have other ideas to suggest, please post.
Thanks to #nikoshr advise this.$el, in view1.j, is defined and when I call render in view1.js the this.$el is fill properly
but it is not attached to the body of document.
How can I make it without using append or similar jquery methods to my main-template.html ?
Here my render function:
render: function ()
{
this.$el.html(Mustache.render(myTemplate, this.view);
}
You are attaching your subviews to elements that do not exist at the time you require them. Something like this may be a step in the right direction:
mainView.js
define([
"js/views/view1",
"js/views/view2",
"js/views/view3",
"text!templates/main-template.html"
], function (View1, View2, View3, mainTemaplte) {
var MainView = Backbone.View.extend({
initialize: function ()
{
this.render();
},
render: function ()
{
// render from template and assign this.el to the root of the element
// e.g #project
var html=Mustache.render(mainTemaplte);
this.setElement( $(html) );
// create each view with its el set to the correct descendant
this.view1 = new View1( _.extend( {el:this.$("#view1")} , this.options) );
this.view2 = new View2( _.extend( {el:this.$("#view2")} , this.options) );
this.view3 = new View3( _.extend( {el:this.$("#view3")} , this.options) );
}
});
return MainView;
});
view1.js
define([… ], function (..) {
var View1 = Backbone.View.extend({
initialize: function () {
console.log(this.$el);
}
});
return View1;
});
And you can recreate your view with something like
require(["js/views/mainView"], function(MainView) {
var view=new MainView();
console.log(view.$el);
//attach the view to the DOM
$("body").append(view.$el);
});
Related
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();
I've got a Backbone.View that renders a collection and filters it on mouse click. I need to add class active to the button that I click, but the problem is that buttons are the part of this view and whenever I try to addClass or toggleClass it just renders again with default class. Here's my view:
var ResumeList = Backbone.View.extend({
events: {
'click #active': 'showActive',
'click #passed': 'showPassed'
},
initialize: function () {
this.collection = new ResumeCollection();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
this.$el.html(this.template({ collection: this.collection.toJSON() });
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function () {
this.$('#active').toggleClass('active');
// a function that returns a new filtered collection
var filtered = this.collection.filterActive();
this.render(filtered);
}
});
But as I've already told, the class I need is toggled or added just for a moment, then the view is rendered again and it is set to default class. Is there any way to handle this?
I simplified the rendering and added some optimizations.
Since we don't have your template, I changed it to enable optimization:
<button id="active" type="button">Active</button>
<button id="passed" type="button">Passed</button>
<div class="list"></div>
Then your list view could be like this:
var ResumeList = Backbone.View.extend({
events: {
'click #active': 'showActive',
'click #passed': 'showPassed'
},
initialize: function() {
this.childViews = [];
this.collection = new ResumeCollection();
},
render: function() {
this.$el.html(this.template());
// cache the jQuery element once
this.elem = {
$list: this.$('.list'),
$active: this.$('#active'),
$passed: this.$('#passed')
};
this.renderList(); // default list rendering
return this;
},
renderList: function(collection) {
this.elem.$list.empty();
this.removeChildren();
collection = collection || this.collection.models;
// Underscore's 'each' has a argument for the context.
_.each(collection, this.renderItem, this);
},
renderItem: function(model) {
var view = new ResumeView({ model: model });
this.childViews.push(view);
this.elem.$list.append(view.render().el);
},
showActive: function() {
this.elem.$active.toggleClass('active');
var filtered = this.collection.filterActive();
this.renderList(filtered);
},
/**
* Gracefully call remove for each child view.
* This is to avoid memory leaks with listeners.
*/
removeChildren: function() {
var view;
while ((view = this.childViews.pop())) {
view.remove();
}
},
});
Additional information:
Managing Views and Memory Leaks
Underscore's each (notice the third argument)
Try to avoid callback hell, make the callbacks reusable (like renderItem)
I have edited the snippet can you try this.
var ResumeList = Backbone.View.extend({
events: {
'click #active': 'filterActive',
'click #passed': 'showPassed'
},
toggleElement: undefined,
initialize: function () {
this.collection = new ResumeCollection();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
this.$el.html(this.template({ collection: this.collection.toJSON() });
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
filterActive: function (evt) {
this.toggleElement = this.$el.find(evt.currentTarget);
// a function that returns a new filtered collection
var filtered = this.collection.filterActive();
this.render(filtered);
this.toggleActive();
},
toggleActive: function() {
if(this.toggleElement.is(':checked')) {
this.$el.find('#active').addClass('active');
} else {
this.$el.find('#active').removeClass('active');
}
}
});
Please note: I have taken checkbox element instead of button.
I've got a collection view with two filter methods, and a render method which takes a parameter. The problem I'm stuck with is that when rendering the view for the first time it returns me an error. Here's my collection:
var ResumeCollection = Backbone.Collection.extend({
url: 'http://localhost:3000',
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
}
});
And my view:
var ResumeList = Backbone.View.extend({
events { // hash array of filter events },
initialize: function () {
this.collection.fetch();
},
render: function (filtered) {
var self = this;
var data;
if (!filtered) {
data = this.collection.toArray();
} else {
data = filtered.toArray();
}
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
},
showActive: function (ev) {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function (ev) {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
},
showAll: function (ev) {
this.render(this.collection);
}
});
This view gets rendered for the first time in my router by passing a collection:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function () {
this.layout = new LayoutView();
}
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
});
And this is the layout view within which all the other views are rendered:
var LayoutView = Backbone.View.extend({
el: $('#outlet'),
render: function (view) {
if (this.child && this.child !== view) {
this.child.undelegateEvents();
}
this.child = view;
this.child.setElement(this.$el).render();
return this;
}
});
When I just refresh my page, I get filtered.toArray is not a function error and nothing is rendered respectively. After inspecting everything in the debugger, I found out that when the view gets rendered for the first time, the filtered attribute receives an empty collection, assigns it to data variable, which becomes an empty array and goes to the body of render function, becoming undefined after that. The mysteries go here: whenever I click items, that are bound to my show* events, they act exactly as expected and render either models where interviewed === false, or true or the whole collection. This looks kinda magic to me and I haven't got the faintest idea what can I do with that.
ADDED: GitHub repo with this project
Your home function on the AppRouter has a typo. You have an extra semi-colon.
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection();
}));
}
Should be
home: function () {
this.layout.render(new ResumeList({
collection: new ResumeCollection()
}));
}
I needed to remove it to get the JSFiddle working: https://jsfiddle.net/4gyne5ev/1/
I'd recommend adding some kind of linting tool into your IDE or Build process (http://eslint.org/)
You need to add home url content to your db.json file like this
"" : [
{
'somthing': 'somthing'
}
]
After a piece of advice from my mentor I realized that the core of the problem was in asynchronous origin of fetch method -- as I passed this.collection.fetch in my initialize function, it executed after my render method, not before it, so my render method had just nothing to render when the view was called for the first time. So, this fix worked:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection();
// removed .fetch() method from here
},
render: function (filtered) {
var self = this;
var data;
// and added it here:
this.collection.fetch({
success: function (collection) {
if (!filtered) {
data = collection.toArray();
} else {
data = filtered.toArray();
}
self.$el.html(self.template(collection.toJSON()));
_.each(data, function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
})
}
});
}
});
And this worked perfectly and exactly as I needed.
I am new to Backbone and have an issue from almous beginning wondering how to overcome issue with unexpected behavior of a main view.
1. I launch page and it looks okey.
2. I click button that lead me to different view and shows me it well.
3. I click "back" button and I see a blank page, but in DOM I can find some elements from previous view, but not visible.
Here is a piece of my router code (I hope this pieces will be enough):
home: function () {
if (!app.leftMenuView) {
app.leftMenuView = new app.views.LeftMenuView({
el: $("#left_menu")
});
} else {
app.leftMenuView.delegateEvents();
}
if (!app.homeView) {
app.homeView = new app.views.HomeView({
el: $("#main_container")
});
} else {
app.homeView.delegateEvents();
}
if (!app.topMenuView) {
app.topMenuView = new app.views.topMenuView({
el: $("#top_menu")
});
} else {
app.topMenuView.delegateEvents();
}
},
search: function () {
app.searchView = new app.views.SearchView({
el: $("body")
});
},
A piece of main html file:
<body>
<div id="search-div"></div>
...
</body>
HomeView:
app.views.HomeView = Backbone.View.extend({
initialize: function () {
this.render()
},
render: function () {
var self = this;
$.get('/template/home.html', function (data) {
self.$el.html(_.template(data)({}));
});
return this;
},
});
A SearchView:
app.views.SearchView = Backbone.View.extend({
events: {
"click #left-arrow-icon": "toMainPage"
},
initialize: function () {
this.render();
},
render: function () {
var self = this;
$.get('/template/searchView.html', function (data) {
self.$el.html(_.template(data));
});
return this;
},
toMainPage: function () {
Backbone.history.history.back();
},
});
In the example provided, the search view, if rendered, will destroy the main html file content since the element provided is directly the body. In any case, if you go to search view, your home page will have been destroyed or will not be able to render anymore.
I would suggest an improved layout management :
separate elements per views
show/hide of root elements based on routes
Hey so I am using backbone localstorage and every time someone hits the search button I want to clear the localstorage so I can just add the new data to the localStorage.
Also, trying to figure out how to then redirect the user to a new view after the success callback in for the localstorage being set, I know there is view.remove() but I am not sure how to use that being that the callback is within the view and also, where/how to render the new view...
Let's say the new view is PageView...
Here is the code for the current search view:
define([
'jquery',
'underscore',
'backbone',
'models/search',
'text!templates/search.html',
], function($, _, Backbone, SearchM, SearchT){
var Search = Backbone.View.extend({
model: SearchM,
el: $("#Sirius"),
events: {
'submit #searchMusic': 'search'
},
search: function (e) {
e.preventDefault();
//create new instance of the model
searchM = new SearchM();
//post instance to the server with the following input fields
searchM.save({
channel: this.$('#channel').val(),
week: this.$('#week').val(),
year: this.$('#year').val(),
filter: this.$('#filter').val()
},{success: storeMusic});
// on success store music on client-side localStorage
function storeMusic (model, response, options) {
console.log('store');
//create new instance of the localStorage with the key name
searchM.localStorage = new Backbone.LocalStorage("music");
clearLocalStorage();
saveToLocalStorage(response);
};
function clearLocalStorage () {
console.log('clear');
//removes the items of the localStorage
this.localStorage.clear();
//pops out the first key in the records
searchM.localStorage.records.shift();
};
function saveToLocalStorage (response) {
console.log('save');
searchM.save({music: response}, {success: nextPage});
};
function nextPage () {
console.log('entered next page');
searchM.set('display', true);
};
},
render: function () {
}
});
return Search;
});
Container view:
define([
'jquery',
'underscore',
'backbone',
'views/search',
'text!templates/search.html'
], function($, _, Backbone, SearchV, SearchT){
var Container = Backbone.View.extend({
el: $("#Sirius"),
render: function () {
var search = new SearchV();
this.$el.html( SearchT );
this.listenTo(searchM, 'change:display', console.log('changed MODEL'));
}
});
return Container;
});
Here is the model:
define([
'underscore',
'backbone'
], function(_, Backbone) {
var Search = Backbone.Model.extend({
url: '/music',
defaults: {
display: false
}
});
return Search;
});
----------------EDIT Confused with below
This is the container and SearchM(model), SearchV(view), SearchT(template)...
var Container = Backbone.View.extend({
el: $("#Sirius"),
render: function () {
//Model CREATED
searchM = new SearchM();
//VIEW Created
var search = new SearchV();
this.$el.html( SearchT );
}
});
return Container;
});
This is the search View - so I took out the model from here, but calling this or this.model actually does not work, as searchM is not defined and the model does not seemed to be passed in... I only added the two methods so ignore the rest for now, if I can make these work then everything can follow suit
var Search = Backbone.View.extend({
el: $("#Sirius"),
events: {
'submit #searchMusic': 'search'
},
search: function (e) {
e.preventDefault();
//post instance to the server with the following input fields
searchM.save({
channel: this.$('#channel').val(),
week: this.$('#week').val(),
year: this.$('#year').val(),
filter: this.$('#filter').val()
},{success: storeMusic()});
function nextPage () {
console.log('entered next page');
searchM.set('display', true);
this.listenTo(searchM, 'change:display', console.log('changed MODEL'));
console.log(searchM.display);
};
Try this to get rid of the model:
searchM.destroy();
That's basically the same as in my answer here, but for a single model.
As for the view changing, i would recommend adding a 'display' or 'loaded' variable to the model, which is false by default and set to true, when the data is ready. Then, have the view listen to the 'change:display' event, triggering the render() method when ready. You can delete the old view, as soon as you know the data has changed and replace it with some loading spinner, which then will be replaced by the new data view.
Hope this helped.
Confused parts:
var Container = Backbone.View.extend({
el: $("#Sirius"),
render: function () {
//Model CREATED
searchM = new SearchM();
//VIEW Created
var search = new SearchV({model: searchM});
this.$el.html( SearchT );
}
});
var Search = Backbone.View.extend({
el: $("#Sirius"),
events: {
'submit #searchMusic': 'search'
},
initialize: function () {
this.listenTo(this.model, 'change:display', this.displayChanged);
},
displayChanged: function () {
console.log('display changed');
},
search: function (e) {
e.preventDefault();
//post instance to the server with the following input fields
searchM.save({
channel: this.$('#channel').val(),
week: this.$('#week').val(),
year: this.$('#year').val(),
filter: this.$('#filter').val()
},{success: storeMusic()});
},
nextPage: function () {
console.log('entered next page');
searchM.set('display', true);
console.log(searchM.display);
},
I haven't used Backbone.LocalStorage before, and the documentation doesn't specify how you should clear the data, however, in the source code there is a _clear() method that should do the trick:
function listStore (model, response, options) {
searchM.localStorage = new Backbone.LocalStorage("music");
searchM.localStorage._clear();
searchM.save({music: response}, {success: console.log('success')
});
As for switching to a new View, that is generally handled using a Backbone.Router which will handle redirecting your users to any area of your application you wish.
var MyRouter = Backbone.Router.extend({
routes: {
"search/:query": "search", // #search/kiwis
"page": "page" // #page
},
page: function() {
new PageView(); //etc...
},
search: function(query) {
...
}
});
//this line is required to tell Backbone that your routes are ready
Backbone.history.start();
Once you have the appropriate routes established, you can navigate to the desired location by calling:
function listStore (model, response, options) {
//check to see if the LS exists, and clear it if so
if(searchM.localStorage){
searchM.localStorage._clear();
}
searchM.localStorage = new Backbone.LocalStorage("music");
searchM.save({music: response}, {success: console.log('success');
searchM.on('sync', function(){
MyRouter.navigate("page", {trigger: true});
});
});