I'm trying to make a SPA (single page app) with Mithril.js.
So far I've found very good tutorial here, and of course on Mithril homepage, but still cannot achieve combination of those two.
Here is modified working example from Dave's guide...
function btn(name, route){
var click = function(){ m.route(route); };
return m( "button", {onclick: click}, name );
}
function Page(content){
this.view = function(){
return [
m("page",
m("span", Menu.menu())
)
, m("div", content)
];
}
}
var Menu = {
menu: function(){
return [
btn("Home", "/home")
, btn("About", "/about")
];
}
};
var page_Home = new Page("The home of the Hobbits. Full of forests and marshes.");
var page_About = new Page(["The blighted home of Sauron. Scenic points of interest include:"]);
m.route(document.body, "/home", {
"/home": page_Home,
"/about": page_About
});
My JSON file:
[
{
"id":1,
"title": "Home",
"url": "/home",
"content":"This is home page"
},{
"id":2,
"title": "About",
"url": "/about",
"content":"This is about page"
},{
"id":3,
"title": "Galery",
"url": "/galery",
"content":"This is gallery page"
}
]
And my effort in combining those two from above:
//model
var PageSource = {
list: function() {
return m.request({method: "GET", url: "pages.json"});
}
};
var pages = PageSource.list();
var App = {
//controller
controller: function() {
return {
menu: pages
, rotate: function() { pages().push(pages().shift()); }
, id: m.route.param(pages.url)
}
},
//view
view: function(ctrl) {
return [
m("header"
, m("h1", "Page Title")
, m("span",
ctrl.menu().map(function(item) {
var click = function(){
console.log (item.url);
m.route(item.url);
};
return [
m("button", {onclick: click}, item.title)
];
})
)
, m("hr")
)
, m("button", {onclick: ctrl.rotate}, "Rotate links" )
, m("p", ctrl.content ) //CONTENT
];
}
};
//initialize
m.route(document.body, "/home", {
"/:id": App
});
And finally, questions are:
- "How can I retrieve data from JSON file and display it in div based on selected button (routing)?"
- "When I use m.route my entire view refreshes, but I only want to reload changed div. How?"
Please help, 'cause so far I really like mithril.js
You're close.
It looks like your router is configured twice, where the latter declaration will overwrite the first. Declare your routes with m.route once, and after the other code has been declared.
When you attempt to reference ctrl.content in your App view, it will be undefined as you haven't defined a content property in the App controller. Add whatever you want to be the content property into the object that the App controller returns.
Thanks to #dcochran I've managed to achieve this:
//model
var PageSource = {
list: function() {
return m.request({method: "GET", url: "pages.json"});
}
};
var pages = PageSource.list();
var id = m.prop()
, url = m.prop()
, title = m.prop()
, content = m.prop();
var App = {
//controller
controller: function() {
return {
menu: pages
, rotate: function() { pages().push(pages().shift()); }
}
},
//view
view: function(ctrl) {
return [
m("header"
, m("h1", "Page title")
, m("span",
ctrl.menu().map(function(item) {
return [ btn(item.title, item.url) ];
function btn(name, route){
var isCurrent = (url === route);
var click = function(){
//m.route(route);
id = item.id;
url = item.url;
content = item.content;
title = item.title;
};
return m(
"button"+(isCurrent ? ".active" : ""),
{onclick: click},
name
);
}
})
)
, m("hr")
)
, m("button", {onclick: ctrl.rotate}, "Rotate links" )
, m(".page", content )
];
}
};
//initialize
m.route.mode = "hash";
m.route(document.body, "/home", {
"/:url": App
})
Related
I'm dividing my functions/objects into service and factory methods, and injecting them into my controller patentTab. I had a code for a tab panel which I originally placed within the controller patentTab that worked.
Now I have placed this code in a factory method and for some reason the content isn't loading. Console log shows no errors, and when I click the relative tab the correct URL is loaded, but the content doesn't change. Is there an issue with my array in the factory? If not, what is the reason?
Orginal code
app.controller('patentTab', function($scope, $http){
$scope.tabs = [{
title: 'Patent Information',
url: 'patent-info.htm'
}, {
title: 'Cost Analysis',
url: 'cost-analysis.htm'
}, {
title: 'Renewal History',
url: 'renewal-history.htm'
}];
$http.get('../json/patent-info.json').then(function(response){
$scope.patentData = response.data.patentInfo;
})
$scope.currentTab = 'patent-info.htm';
$scope.onClickTab = function (tab) {
$scope.currentTab = tab.url; //the tabs array is passed as a parameter from the view. The function returns the url property value from the array of objects.
}
$scope.isActiveTab = function(tabUrl) {
return tabUrl == $scope.currentTab;
}
});
New code (with issue)
app.controller('patentCtrl', ['$scope', '$http', 'patentTabFactory', function($scope, $http, patentTabFactory) {
$http.get('http://localhost:8080/Sprint002b/restpatent/').then(function(response) {
$scope.patents = response.data;
});
$scope.loadPatentItem = function(url) {
$scope.patentItem = url;
}
$scope.tabs = patentTabFactory.tabs;
$scope.currentTab = patentTabFactory.currentTab;
$scope.onClickTab = patentTabFactory.onClickTab;
$scope.isActiveTab = patentTabFactory.isActiveTab;
}]);
app.factory('patentTabFactory', function() {
var factory = {};
factory.tabs = [{
title: 'Patent Information',
url: 'patent-info.htm'
}, {
title: 'Cost Analysis',
url: 'cost-analysis.htm'
}, {
title: 'Renewal History',
url: 'renewal-history.htm'
}];
factory.currentTab = 'patent-info.htm';
factory.onClickTab = function (tab) {
factory.currentTab = tab.url; //the tabs array is passed as a parameter from the view. The function returns the url property value from the array of objects.
console.log(tab.url);
}
factory.isActiveTab = function(tabUrl) {
return tabUrl == factory.currentTab; //for styling purposes
}
return factory;
});
You not calling factory.onClickTab() method from your controller.
It should be like :
$scope.onClickTab = function(currentTab) {
patentTabFactory.onClickTab(currentTab);
$scope.currentTab = patentTabFactory.currentTab;
};
and, for isActiveTab, Like :
$scope.isActiveTab = patentTabFactory.isActiveTab(currentTab);
Here is a plunker where I am using a factory. The only changes I have done are:
1. Place the factory file before the app script file.
2. Use a separate declaration for factories and then inject it in the app.
var factories = angular.module('plunker.factory', []);
factories.factory('patentTabFactory', function() {
// Factory bits
};
I have injected the factories in the app.
var app = angular.module('plunker', ['plunker.factory']);
Here is a working plunker for that. PlunkR
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();
}
};
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);
}
});
});
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
};
});
I've recently begun taking backbone.js for a test spin. I've got a little app that is supposed to grab data from the server, turn the data into form fields, and insert them into a form. Unfortunately, when I go to render() the collection view, nothing happens. However, after the page loads, if I type
fieldCollectionView.render();
into the console, magically my fields render.
Here's my app code. It's the penultimate line that I am expecting to work, but it doesn't:
var FieldModel = Backbone.Model.extend({
defaults: function(){
return {
type: "text",
}
},
});
var FieldCollection = Backbone.Collection.extend({
model: FieldModel,
url: "/register/Backbone/includes/JSONgetRegFields.js",
//parse: function(response){
// return response
//}
});
var FieldView = Backbone.View.extend({
tagName: "div",
className: "field_wrapper",
id: "field_wrapper1",
template: _.template( $("#field-template").html() ),
render: function() {
var attributes = this.model.toJSON();
this.$el.html( this.template(attributes) );
return this;
}
});
var FieldCollectionView = Backbone.View.extend({
initialize: function() {
},
addOne: function( fieldModel ) {
var fieldView = new FieldView({model: fieldModel });
//console.log(fieldView.render().el )
this.$el.append( fieldView.render().el );
},
render: function(){
this.collection.forEach(this.addOne, this);
//console.log(this)
}
});
var fieldCollection = new FieldCollection();
var fieldCollectionView = new FieldCollectionView({ collection: fieldCollection });
fieldCollection.fetch();
fieldCollectionView.render()
$("#formTarget").html( $(fieldCollectionView.el) );
And here is what my returned json looks like:
[ { "label": "First Name", "fid": "FirstName", "fieldClasses" : "required", "labelClasses" : "requiredLabel"}
, { "label": "Last Name", "fid": "LastName", "fieldClasses" : "required", "labelClasses" : "requiredLabel"}
, { "label": "Email", "fid": "Email", "fieldClasses" : "required email", "labelClasses" : "requiredLabel"}
, { "label": "Confirm Email Please", "fid": "ConfEmail", "fieldClasses" : "required email", "labelClasses" : "requiredLabel"}
, { "label": "Fax", "fid": "Fax", "fieldClasses" : "number cdsphone", "labelClasses" : ""}
]
Also I should note that if I used
setTimeout( function() { fieldCollectionView.render() }, 120)
My collection view renders as well - But I don't feel like that is a great way to proceed. Where have I gone wrong here?
You are almost certainly rendering your view before the data in the collection has arrived. You need to understand the asynchronous nature of backbone's AJAX calls and controlling code flow via events. You didn't paste your FieldCollectionView class, but that needs something like this:
var FieldCollectionView = Backbone.View.extend({
initialize: function () {
this.listenTo(this.collection, 'sync', this.render);
}
});
Then you can be sure when the data arrives, the view will rerender to show the new data.
This is one option open to you, add a listener to your views collection.
var FieldCollectionView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
},
addOne: function( fieldModel ) {
var fieldView = new FieldView({model: fieldModel });
//console.log(fieldView.render().el )
this.$el.append( fieldView.render().el );
},
render: function(){
this.collection.forEach(this.addOne, this);
//console.log(this)
}
});