Anchoring to an element with a javascript hash router - javascript

I'm using Parse-SDK-JS, Handlebars.js and hash routing to create a dynamic webpage. When a user clicks on any link, I call a template using a URL in the following way: http://www.website.com/#/admin.
Router
BlogApp.Router = Parse.Router.extend({
start: function () {
Parse.history.start({root: '/beta/'});
},
routes: {
'': 'index',
'blog/:url': 'blog',
'category/:url': 'category',
'admin': 'admin',
'login': 'login',
'reset': 'reset',
'logout': 'logout',
'add': 'add',
'register': 'register',
'editprofile': 'editprofile',
'changeprofilepic': 'changeprofilepic',
':username': 'userprofile'
},
index: function () {
BlogApp.fn.setPageType('blog');
$blogs = [];
if (!currentUser) {
Parse.history.navigate('#/register', {trigger: true});
console.log("There is no logged in user.");
} else {
var groupId = currentUser.get('groupId');
var designsQuery = new Parse.Query(BlogApp.Models.Blog).equalTo('groupId', groupId).include('author').descending('lastReplyUpdatedAt').limit(50);
designsQuery.find({success: function (blogs) {
for (var i in blogs) {
var des = blogs[i].toJSON();
des.author = blogs[i].get('author').toJSON();
$blogs.push(des);
}
// console.log(blogs);
BlogApp.fn.renderView({
View: BlogApp.Views.Blogs,
data: {blogs: $blogs}
});
}, error: function (blogs, e) {
console.log(JSON.stringify(e));
}});
}
},
});
View
BlogApp.Views.Blogs = Parse.View.extend({
template: Handlebars.compile($('#blogs-tpl').html()),
className: 'blog-post',
render: function () {
var collection = {blog: []};
collection = {blog: this.options.blogs};
this.$el.html(this.template(collection));
},
});
My problem is that upon loading a new template, the user is not sent to the top of the page, i.e. to the following div:
<div id="main-nav"></div>
The users' scroll position on the page doesn't change if the new page is longer than the current page. The user just ends up somewhere down the middle of the page because the new template is loaded but they are not anchoring anywhere new.
Normally in HTML I would open a new page to a particular anchor with something like this: http://www.website.com/page#container if I wanted to, but with the way I set up my hash routing the anchor is the template call itself, so I can't do something like this: http://www.website.com/#/admin#container.
I hope this makes sense.
How can I always send the user to the div "container" upon loading a new template into my view?

I solved this by scrolling into an element after the View was generated.
cookies: function () {
BlogApp.fn.setPageType('cookies');
BlogApp.fn.renderView({
View: BlogApp.Views.Cookies
});
document.getElementById('main-nav').scrollIntoView();
},
Better... by adding the scrollIntoView() function after data is rendered into the View object, so that this works for all links in the router without so much copy pasta.
BlogApp.fn.renderView = function (options) {
var View = options.View, // type of View
data = options.data || null, // data obj to render in the view
$container = options.$container || BlogApp.$container, // container to put the view
notInsert = options.notInsert, // put the el in the container or return el as HTML
view = new View(data);
view.render();
if (notInsert) {
return view.el.outerHTML;
} else {
$container.html(view.el);
document.getElementById('main-nav').scrollIntoView();
}
};

Related

Model method error while trying to navigate

I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.

Marionette CompositeView undelegating childview events on setElement

I have a marionette compositeview which I am using to create a item list for a profile page on an app. For the child view, I extend from an already existing ItemView.
When I use this.setElement(this.el.innerHTML) in the compositeview onRender function, all the events set in the child view no longer are triggered and even more so, triggering them in the console on the inspector tool in the browser, does nothing.
However when I do not use setElement, the container div is added to my markup, but now all the events in the child view work.
Can someone help me understand this please.
The Collection I am using has a custom clone method.
I am using a global collection which is updated and stored in cache on each fetch.
When I actually instantiate my view, the collection has already been used and a region in the main layout view has been populated with a item list similar to the one I want to render.
This is how I instantiate my view:
var currentUser = Profile.get('username');
// Perform changes to global collection
Items.url = API + '/items/search?q=' + currentUser + '&size=20';
Items.parse = function (response) {
if (!response.results) {
return response;
} else {
return response.results;
}
};
Items.fetch(
{success: function (collection, response, options) {
this.listOfItems = new View.itemListProfilePage({
template: TemplIds.profilePagePostedItems,
parentClass: 'profile-cols',
collection: Items, // global collection
filterAttr: {user: currentUser},
isFiltered: true,
lazyLoad: true,
childViewContainer: '#profile-items',
childView: View.itemProfilePage.extend({
template: TemplIds.item
})
});
Backbone.trigger('main:show', this.listOfItems); //'main:show' is an event in layoutview which calls region.show
},
remove: false
});
My compositeview:
View.itemListProfilePage = Marionette.CompositeView.extend({
collection: null, //original collection cloned later for filtering
fetch: null, //promise for fetched items
lazyView: null,
options: {
parentClass: '',
filterAttr: {},
isFiltered: false,
lazyLoad: false
},
initialize: function () {
this.stopListening(this.collection);
//Change collection property and re-apply events
this.collection = this.collection.clone(this.options.filterAttr, this.options.isFiltered);
this._initialEvents();
this.collection.reset(this.collection.where(this.options.filterAttr), {reset: true});
this.listenTo(Backbone, 'edit:profileItems', this.addEditClassToSection);
},
onRender: function () {
this.setElement(this.el.innerHTML, true);
},
onShow: function () {
if (this.options.parentClass) {
this.el.parentElement.className = this.options.parentClass;
}
},
addEditClassToSection: function (options) {
if ( options.innerHTML !== 'edit' ) {
this.el.classList.add('edit-mode');
} else {
this.el.classList.remove('edit-mode');
}
},
}
The parent ItemView:
View.Item = Marionette.ItemView.extend({
model: null,
numLikes: null, //live set of DOM elements containing like counter
modalItem: null, //view class with further details about the item to be used within a modal
events: {
'click img.highlight': 'showModal'
},
initialize: function (options) {
var itemWithHeader; //extended item view class with header at the top and no footer
var addToCart;
//Set up all like-related events
this.listenTo(this.model, "change:numLikes", this.updateNumLikes);
this.listenTo(this.model, "change:liked", this.updateLiked);
//Set up the view classes to be used within the modal on click
itemWithHeader = View.ItemWithHeader.extend({
template: this.template,
model: this.model //TODO: move to inside itemDetails
});
itemAddToCart = View.ItemAddToCart.extend({
template: TemplIds.itemAddCart,
model: this.model //TODO: move to inside itemDetails
});
this.modalItem = View.ItemDetails.extend({
template: TemplIds.itemDetails,
model: this.model,
withHeader: itemWithHeader,
addToCart: itemAddToCart
});
},
onRender: function () {
var imgContainerEl;
var likeButtonEl;
//Get rid of the opinionated div
this.setElement(this.el.innerHTML);
this.numLikes = this.el.getElementsByClassName('num');
//Add the like button to the image
likeButtonEl = new View.LikeButton({
template: TemplIds.likeButton,
model: this.model
}).render().el;
this.el.firstElementChild.appendChild(likeButtonEl); //insert button inside img element
},
showModal: function (evt) {
var modalView = new View.Modal({
views: {
'first': {view: this.modalItem}
}
});
Backbone.trigger('modal:show', modalView);
},
});
The itemView for each individual item in my list:
View.itemProfilePage = View.Item.extend({
events: _.extend({},View.Item.prototype.events, {
'click .delete-me': 'destroyView'
}
),
onRender: function () {
View.Item.prototype.onRender.call(this);
this.deleteButtonEl = new View.itemDeleteButton({
template: TemplIds.deleteButton
}).render().el;
this.el.firstElementChild.appendChild(this.deleteButtonEl);
},
destroyView: function (evt) {
this.model.destroy();
}
});
The short answer is that you should not be using setElement.
Backbone specifically uses the extra container div to scope/bind the view's events. When you use setElement you are changing what the parent element is. Since you are doing this in the onRender function, which is called after the template has been rendered and the events have already been bound, you are losing your event bindings.
The correct thing to do if you are going to use Marionette and Backbone is to expect and utilize the "extra" div wrapper that is generated when you render a view. You can take control of the markup for that "wrapper" div by using className, id, and tagName view properties on your view classes.

Backbone rendering but plugin doesn't work when using router

I am using a plugin for dropdowns found here: http://patrickkunka.github.io/easydropdown/
I've got it working in Backbone but I had to activate it manually and make sure it runs after the render is complete. It works when I refresh the page but if i leave the page and then come back to it the plugin does not take effect. The render function is running when each time so i dont know why it wont work when im navigating normally.
render: function() {
setTimeout(function(){
$(function(){
var $selects = $('select');
$selects.easyDropDown({
cutOff: 5,
wrapperClass: 'dropdown',
onChange: function(selected){
// do something
}
});
});
}, 0);
console.log("Rendering");
this.$el.html(template());
return this;
}
Here is my router code:
return Backbone.Router.extend({
initialize: function() {
// Render the layout view only once and simple change the contents of #content
// as per the desired route
var $body = $('body');
var layoutView = new LayoutView({ el: $body }).render();
this.$el = $("#content", $body);
this.currentView = null;
// Init the subrouters
this.bookRouter = this.addSubRouter(BookRouter, "books");
this.quoteRouter = this.addSubRouter(QuoteRouter, "quotes");
this.employeeRouter = this.addSubRouter(EmployeeRouter, "employees");
this.saleRouter = this.addSubRouter(SaleRouter, "sales");
// When the route changes we want to update the nav
this.bind("route", _.bind(this.updateNav, this));
},
// These are the base routes
// Other routes can be attached by creating subroutes
routes: {
// viewIndex is the main site index
// All other routes are handled by sub-routers
"": "viewIndex",
"upload": "upload",
"export": "export",
"test": "test",
},
// Add a sub route at the given route and listen for events
addSubRouter: function(subRouterClass, route) {
var router = new (subRouterClass)(route, { createTrailingSlashRoutes: true });
router.on("view", _.bind(this.switchView, this));
router.on("route", _.bind(function(route, section) {
this.trigger("route", route, section);
}, this));
return router;
},
// Change from this.currentView to newView
switchView: function(newView) {
// Do we need to remove the old view?
if (this.currentView) {
this.currentView.remove();
}
this.currentView = newView;
// Add the new view
this.$el.append(newView.render().$el);
newView.addedToDOM();
},
updateNav: function(route, section) {
// Get hold of the nav element
var $nav = $("#nav");
// Clean up the route string
route = route.replace("route:", "");
// Remove the currently active item
$(".active", $nav).removeClass("active");
// Apply .active to any navigation item that has a matching data-route attribute
$("[data-route=\"" + route + "\"]", $nav).addClass("active");
},
viewIndex: function () {
var view = new IndexView();
this.switchView(view);
},
upload: function (){
var view = new UploadIndexView();
this.switchView(view);
},
export: function() {
var view = new ExportIndexView();
this.switchView(view);
},
test: function() {
var view = new TestIndexView();
this.switchView(view);
}
});
});

Backbone.js - History is creating two entries

I'm not sure how to express this in code, as I can't seem to locate the problem, but my issue is that Backbone.history seems to be recording two items when a user clicks on a list item in my app.
This is not consistent.
My app has a 4 item navigation at the bottom that links to 4 main sections (the first one being home - routed to '/'). If I load up the app, go to one of the other navigation pages, then click the 'Home' button again and then click one of the navigation options I get a list of items to choose from. If I then choose one two entries are added - Firstly, for some reason, a reference to the home route with /# at the end and then the route for the item I clicked.
The end result is that 'back' then inexplicably takes me to the home page.
If it helps, my router looks like this...
var siansplanRouter = Backbone.Router.extend({
initialize: function () {
var that = this;
this.routesHit = 0;
//keep count of number of routes handled by your application
Backbone.history.on('route', function() { that.routesHit++; }, this);
window.SiansPlanApp.render();
window.SiansPlanApp.router = this;
},
routes: {
'': 'showHome',
'home': 'showHome',
'hub': 'showHome',
'samples': 'showJqmSamples',
'mealplanner': 'showCurrentMealPlanner',
'mealplanner/:planId': 'showMealPlanner',
'recipes': 'showRecipeSearch',
'recipes/:recipeId': 'showRecipe',
'settings': 'showSettings',
'versioninfo': 'showVersionInfo',
'*other': 'showHome'
},
routesHit: 0,
back: function() {
if(this.routesHit > 1) {
window.history.back();
} else {
//otherwise go to the home page. Use replaceState if available so
//the navigation doesn't create an extra history entry
this.navigate('/', { trigger: true, replace: true });
}
},
showHome: function () {
SiansPlanApp.renderHome();
},
showJqmSamples: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Hub.Samples());
},
showMealPlanner: function (planId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ id: planId }));
},
showCurrentMealPlanner: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ current: true }));
},
showRecipeSearch: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Search());
},
showRecipe: function (recipeId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Recipe({ id: recipeId }));
},
showSettings: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.System.Settings());
},
showVersionInfo: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.About.VersionInfo.ListView());
}
});
I've got some basic elements in a kick off file too here...
define(['router', 'regions/r-app', 'jquery', 'domReady'],
function (SiansPlanRouter, AppRegion) {
var run = function () {
// Global click event handler to pass through links to navigate
$(document).on("click", "a:not([data-bypass])", function (e) {
var href = { prop: $(this).prop("href"), attr: $(this).attr("href") };
var root = location.protocol + "//" + location.host + SiansPlanApp.root;
if (href.prop && href.prop.slice(0, root.length) === root) {
e.preventDefault();
Backbone.history.navigate(href.attr, true);
}
});
$.ajaxPrefilter(function (options, originalOptions, jqXhr) {
//options.url = '/api' + options.url;
});
// Create the global namespace region object.
window.SiansPlanApp = new AppRegion();
// Adds the authorization header to all of the API requests.
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("Authorization", 'SiansPlan ' + SiansPlanApp.cookies.getSessionData());
});
// Load up session data if any is present yet - this can't happen until the XHR headers are set up.
SiansPlanApp.session.loadSession();
// Instantiate the router.
window.SiansPlanApp.router = new SiansPlanRouter();
// Boot up the app:
Backbone.history.start();
};
return {
run: run
};
});

Re-render a view

What is the best way to re-render a view after an event takes place (eg. submitting a note). In the below code, I want to re-render the view to show the note that was just added.
var NotesView = SectionBaseView.extend({
model: app.models.Note,
events: {
'submit': 'Notes'
},
Notes: function (e) {
e.preventDefault();
var details = new this.model($('form').serializeObject());
details.url = '/email/Notes';
details
.save()
.done(function () {
app.notifySuccess('Note added successfully.');
});
views['#Notes'].render();
}
});
Notes view is initialized in the document.ready function as follows:
views['#Notes'] = new NotesView({ el: '#Notes', template: app.templates.Notes });
I tried using views['#Notes'].render(); but this doesn't seem to be working.
The default implementation of render is a no-op. Override this function with your code that renders the view template from model data, and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls. Docs
var NotesView = SectionBaseView.extend({
model: app.models.Note,
events: {
'submit': 'Notes'
},
render : function(){
//your code
return this;
},
Notes: function (e) {
var that = this;
e.preventDefault();
var details = new this.model($('form').serializeObject());
details.url = '/email/Notes';
details
.save()
.done(function () {
app.notifySuccess('Note added successfully.');
});
that.render();
}
});
on document.ready
views['#Notes'] = new NotesView({ el: '#Notes', template: app.templates.Notes });
views['#Notes'].render();

Categories