I have a Backbone App with a large router. I use the Backbone Layout manager to load different layouts depending on what subpage I'm on. My problem is, that my top navigation gets rendered once again, each time the subpage gets rendered. So how can I avoid this?
My router:
routes: {
'': 'index',
'home': 'home',
':name' : 'artistchannel',
':name/' : 'artistchannel',
':name/videoes': 'artist_videos',
':name/videoes/': 'artist_videos',
':name/videoes?w=:videoid' : 'artist_videos',
':name/releases': 'artist_discography',
':name/releases/': 'artist_discography',
':name/merchandise' : 'artist_merchandise',
':name/concerts': 'artist_concerts'
},
artistchannel: function (params) {
artistController.initArtist(params.name);
},
artist_discography: function(params){
artistController.initDiscography(params.name);
},
and so on...
then I have a controller for each route (here artist and discography page):
ArtistController.prototype.initArtist = function(name) {
this.artistModel = new ArtistModule.Model({slug: name});
this.artistModel.fetch();
this.artistModel.on('sync', function(){
this.artistView = new ArtistModule.View({model: this.artistModel});
App.useLayout('artistchannel', 'artistchannel').setViews({
'.userMenu': this.acuserNav,
'.artistChannelDiv': this.artistView
}).render();
}, this);
window.scrollTo(0,0);
};
ArtistController.prototype.initDiscography = function(name) {
this.artistdiscographyModel = new ArtistDiscographyModule.ArtistDiscographyModel({slug: name});
this.artistdiscographyModel.fetch();
this.artistdiscographyModel.on('sync', function() {
this.artistDiscographyView = new ArtistDiscographyModule.View({model: this.artistdiscographyModel});
App.useLayout('artistDiscography', 'artistDiscography').setViews({
'.userMenu': this.acuserNav,
'.releasesDiv' : this.artistDiscographyView
}).render();
}, this);
window.scrollTo(0,0);
};
The same goes for concerts, merchandise etc.
All subpages (in this case artistchannel.html and artistDiscography.html) have the same menu in the HTML, which I want to avoid, so basically, its repeated code which looks like:
<ul>
<li>
Releasepage
</li>
<li>
Concertpage
</li>
etc. etc.
</ul>
So what I want that the topmenu not gets rerendered all the time. Is it possible to include all inside one single controller?
Have a Layout & ShellView/MenuView. Don't append $el of every view to body, instead use a container for each specific view. One approach can be :
<body>
<div id='menu'></div>
<div id='content'></div>
</body>
new Backbone.Router.extend({
initialize:function(){
new MenuView({el:"#menu"}).render(); //this one creates menu
},
routes:{...},
routeHandler:function(){
new ArtistVideosView({el:'#content'}).render();
}
});
Related
For whatever reason I am unable to solve this issue with countless hours of troubleshooting. I have some simple helpers working with a Bootstrap 3 nav-tabs list.
I want to render a different template based on which list item is active. Here are my helpers:
Template.Profile.helpers({
'personal':function(){
if($('.profile-info').hasClass('active')) {
return true;
} else {
return false;
}
},
'groups':function(){
if($('.profile-groups').hasClass('active')) {
return true;
} else {
return false;
}
},
'commitments':function(){
if($('.profile-commitments').hasClass('active')) {
return true;
} else {
return false;
}
}
});
And here is my HTML:
<ul class="nav nav-tabs">
<li class="active profile-info">Personal Info</li>
<li class="profile-groups">Groups</li>
<li class="profile-commitments">Commitments</li>
</ul>
{{#if personal}}
{{> ProfilePersonal}}
{{else}}
{{#if groups}}
{{> ProfileGroups}}
{{else}}
{{> ProfileCommits}}
{{/if}}
{{/if}}
The helpers will not be re-run when you click a tab, as there is no reactive data change to invalidate the computation.
A more Meteor-ish approach would be to add a reactive variable to hold the tab state and change that in an event listener.
<template name="Profile">
<ul class="nav nav-tabs">
{{#each tabs}}
<li class="{{isActive #index}} profile-{{name}}">{{title}}</li>
{{/each}}
</ul>
{{> Template.dynamic template=tpl}}
</template>
#index references the index of the current loop, and it's provided as an argument to the isActive helper.
Then, your JavaScript file can include a definition for the tabs and the handling code:
var tabs = [{
idx: 0,
name: "info",
title: "Personal Info",
template: "ProfilePersonal"
}, {
idx: 1,
name: "groups",
title: "Groups",
template: "ProfileGroups"
}, {
idx: 2,
name: "commitments",
title: "Commitments",
template: "ProfileCommits"
}];
The tabs are a plain JS array. The following code uses them in the template's context:
Template.Profile.helpers({
// get current sub-template name
tpl: function() {
var tpl = Template.instance();
return tabs[tpl.tabIdx.get()].template;
},
// get the tabs array
tabs: function() {
return tabs;
},
// compare the active tab index to the current index in the #each loop.
isActive: function(idx) {
var tpl = Template.instance();
return tpl.tabIdx.get() === idx ? "active" : "";
}
});
Template.Profile.events({
'click .nav-tabs > li': function(e, tpl) {
tpl.tabIdx.set(this.idx);
}
});
Template.Profile.onCreated(function() {
this.tabIdx = new ReactiveVar();
this.tabIdx.set(0);
});
When the template is created (onCreated()), a new reactive variable is added as an instance variable. This variable can then be accessed in helpers and set in event handlers.
The event handler receives the event object and template instance as parameters and has the data context set as the this pointer; therefore, tpl.tabIdxrefers the reactive variable and this refers to the object that represents the clicked tab (for example,
{
idx: 0,
name: "info",
title: "Personal Info",
template: "ProfilePersonal"
}
for the first tab, as this was the template's data context when the first tab was rendered.
The helper functions get the Template instance using a call to Template.instance(). Then, it queries the value of the reactive array.
This creates a computation in a reactive context (helpers are reactive contexts and they are rerun when the computation they create is invalidated, and that happens when an Mongo cursor, or a reactive variable that is read in the computation is changed).
Therefore, when the reactive variable is set in the event handler, the helpers are re-run and the template reflects the new value.
These are all fundamental to Meteor and are explained in the full Meteor documentation and in many resources.
I use a knockoutjs with templating plugin (Using Underscore Template with Knockout using interpolate due to asp.net)
If i have a header and a list:
<ul data-bind="template: { name: 'people' }"></ul>
<script type="text/html" id="people">
<h2>{{= hdr}}</h2>
{{ _.each(people(), function(item) { }}
<li>{{=item.name }} ({{=item.age }})</li>
{{ }); }}
</script>
also a button
<button id="bindclick">Click</button>
and a ja code where i use knockout:
ko.applyBindings({
hdr: "People",
people: ko.observableArray([{name:"name1",age: 45},{name:"name2",age: 33}])
});
How to do, that template value can be change with clicking a button instead of "Uncaught Error: You cannot apply bindings multiple times to the same element."?:
$("#bindclick").click(function() {
ko.applyBindings({
hdr: "People2",
people: ko.observableArray([{name:"name1",age: 45}])
});
});
Thanks
You should only need to call applyBindings once with a model object.
Later on in your click handler, you would simply update your model.
For example:
var theModel = {
hdr: ko.observable('People'),
people: ko.observableArray([{name:"name1",age: 45},{name:"name2",age: 33}])
};
ko.applyBindings(theModel);
$('#bindclick').click(function () {
theModel.hdr('People2');
theModel.people([{name:"name1",age: 45}]);
});
Updating the model should update your previously bound content.
I would like to load a View and passing a parameter named : layout_version
I'm using JST as my template engine.
Views.BottomBarView = Backbone.View.extend({
el: ".l-bottom-bar",
template: JST['templates/bottom_bar'],
render: function(options) {
this.model.set("layoutVersion", options.layoutVersion);
this.$el.html( this.template(this.model.toJSON()) );
return this;
}
});
The .jst file is as follows:
{{ if (layoutVersion == 1) { }}
<div class="bottom-bar-s-version">Other</div>
{{ } else if { layoutVersion == 2 }}
<div class="bottom-bar-s-version">Show more</div>
{{ } }}
Since I'm not passing any model to the view when creating it, just an object { layoutVersion: 2 } I get this.model is undefined
I'm using the bottom_bar file to hold two different HTMLs inside one file and rendering it depending on the parameter inside the model.
How can I achieve my goal ?
You don't need to pass a model, you can just pass an object to the template.
Views.BottomBarView = Backbone.View.extend({
el: ".l-bottom-bar",
template: JST['templates/bottom_bar'],
render: function(options) {
this.$el.html(this.template( { "layoutVersion": options.layoutVersion }));
return this;
}
});
Just replace this this.model.toJSON() by :
{ "layoutVersion": options.layoutVersion }
I'm learning ember these days and I encountered a problem with link-to helper. If I use it to create a link for nested route it works fine (if click on the link, "active" class will be added to the element - as described in docs) until I reload the page. When I reload the page the content for nested rouse will be loaded to the {{outlet}} properly but link will lose its "active" class. What am I doing wrong?
JavaScript:
window.App = Ember.Application.create({ rootElement: '#app' });
App.Router.map(function () {
this.resource('notes', { path: '/' }, function () {
this.route('show', { path: '/:note_id' });
});
});
App.NotesRoute = Em.Route.extend({
model: function () {
return App.Note.find();
}
});
App.NotesShowRoute = Em.Route.extend({
model: function (params) {
return App.Note.find(params.note_id);
}
});
App.Note = Em.Object.extend();
App.Note.reopenClass({
find: function(id) {
var notes = [
{
id: 1,
title: 'abc',
text: 'lorem ipsum text 1111111'
},
{
id: 2,
title: 'def',
text: 'lorem ipsum text 2222222'
}
];
return id ? notes[parseInt(id) - 1] : notes;
}
});
HTML:
<div id="app" class="row">
<script type="text/x-handlebars">
<div class="col-md-2">
<h2>Tags</h2>
</div>
{{outlet}}
</script>
</div>
<script type="text/x-handlebars" data-template-name="notes">
<div class="col-md-3">
<h2>Notes</h2>
{{#each}}
{{#link-to 'notes.show' this}}{{title}}{{/link-to}}
{{/each}}
</div>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="notes/show">
<div class="col-md-7">
<h2>{{title}}</h2>
<p>{{text}}</p>
</div>
</script>
When you click a link-to, it passes the object to the new route. So the model lookup isn't called. So both the context of the show route and the linked object refer to the same object. So it will get marked as active.
However, when you refresh the page, you're doing the lookup twice, once in the NotesRoute model (which you loop over with each), and once in the NotesShowRoute model.
Javascript objects are reference types. Two plain javascript objects aren't considered equal, even if their content is the same. e.g. try typing this into your javascript console.
{ one: 1, two: 2} == {one: 1, two: 2}
So the object referred to in the link-to isn't the same as the model of the current route. So the equality check for the link being active won't work.
Quick solution is to stop the find from creating the object every time. e.g.
App.Note.reopenClass({
all: [
{
id: 1,
title: 'abc',
text: 'lorem ipsum text 1111111'
},
{
id: 2,
title: 'def',
text: 'lorem ipsum text 2222222'
}
],
find: function(id) {
return id ? this.all[parseInt(id) - 1] : this.all;
}
});
Another options is to roll some sort of identity map for your objects. Here is a blog post doing a much better example than I can of explaining it.
Note I haven't actually tested that code because I'm too lazy to create a jsbin. But let me know if it doesn't work.
I have a collection view and a model view, like so:
EventListView
|-- EventView
EventListView must display many EventViews in a one-to-many relationship. I am using the underscore _.template() function to build my views templates.
Here is my EventView template:
<h1>
<span class="date"><%= prefix %><%= dateString %></span>
<span class="title"><%= title %></span>
</h1>
<div class="caption"><%= caption %></div>
My EventView render method:
render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
And here is my EventListView template:
<h1>
<% if(typeof(title) != "undefined") { print(title) } %>
</h1>
<%= events %>
And it's render method:
// this._EventViews is an array of eventView objects
render: function() {
var templateData = {
events: _.reduce(this._EventViews, function(memo, eventView) { return memo + eventView.$el.html(); }, "")
}
this.$el.html(this.template(templateData));
return this;
}
The problem I am having is that eventView.$el.html() contains only the HTML in my template, but I need to take advantage of the tagName, className and id attributes that Backbone views support.
Consider if I set up EventView like so:
return Backbone.View.extend({
model: EventModel
, tagName: 'article'
, className: 'event'
, template: _.template(templateText)
, render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
I want to insert:
<article class="event" id="someID342">
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
</article>
but eventView.$el returns:
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
How do I insert the entire eventView element? Not just it's innerHTML.
Just reserve placeholder in your EvenListView's template
<h1><%- title %></h1>
<div class="js-events"></div>
And then render and append child views
render: function() {
this.$el.html(this.template({title: 'Title'}));
this.$events = this.$('.js-events');
_.each(this._EventViews, function (eventView) {
this.$events.append(eventView.render().$el);
}, this);
return this;
}
The render() function shouldn't be responsible for handling the setup of the view.el. This is done by Backbone in the _ensureElement function that is called when you initialize the view.
Also, the $.fn.html() function is only supposed to return the contents of the selected element.
You have many options but I think the most flexible and sustainable approach is to get each sub view to define its own template. The parent view simply appends the child elements .el property.
The advantages of this approach, your template is only compiled once. And updates to children do not require re-rendering parent and neighbouring elements.
Here is a JSBin
Example:
var ContainerView = Backbone.View.extend({
tagName: "article",
className: "event",
id: "someID342",
initialize: function(options){
//the template will now be rendered
this.childView = new ChildView()
//the rendered child will now appear within the parent view
this.el.appendChild( this.childView.el )
}
})
var ChildView = Backbone.View.extend({
tagName: "h1",
dateString:"01/02/2010",
prefix: "Date: ",
caption: "What a wonderful date!:",
title: "I am a title",
template: _.template([
'<h1>',
'<span class="date"><%= prefix %><%= dateString %></span>',
'<span class="title"><%= title %></span>',
'</h1>',
'<div class="caption"><%= caption %></div>'
].join("")),
initialize: function(){
this.render()
},
render: function(){
// because you are only altering innerHTML
// you do not need to reappend the child in the parent view
this.el.innerHTML = this.template(this)
}
})
I'd personally caution against using templates in Backbone at all. I've found that simply having a Backbone view for every component of your app becomes a lot easier to edit later. Sharing templates is a lot harder than sharing views. Of course it depends on the requirements of your project.