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);
Related
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 });
},
I have a Backbone Marionette app with Router and a Controller. In my app you can view a collection of texts (index route with collection fetching from server), can view existing collection of texts (indexPage route without fetching from server) and can create a new text (form route). Views of list texts and create form are different from each other and changes in region.
I want to add a successully saved model to a collection and then redirect to indexPage route, but what is the best way to get a texts collection from _FormView success callback? Or how to restruct an app to do it simple?
I can send event to a controller with Backbone.Radio but want to deal without it.
Routes
router.processAppRoutes(controller, {
'': 'index',
'index': 'indexPage',
'create': 'form'
});
Controller
_Controller = Marionette.Controller.extend({
initialize: function () {
this.list = new _MainTexts();
},
index: function () {
if (!_.size(this.list)) {
var
self = this;
this.list.fetch({
success: function (collection, response, options) {
self.indexPage();
return;
}
});
}
this.indexPage();
},
indexPage: function () {
var
textsView = new _TextsView({
collection: this.list
});
application.getRegion('contentRegion').show(textsView);
},
form: function () {
var
formView = new _FormView({
model: new _MainText()
});
application.getRegion('contentRegion').show(formView);
}
});
Views
_TextView = Marionette.ItemView.extend({
className: 'item text',
template: function (serialized_model) {
return _.template('<p><%= texts[0].text %></p>')(serialized_model);
}
});
_TextsView = Marionette.CollectionView.extend({
className: 'clearfix',
childView: _TextView
});
Form view
_FormView = Marionette.ItemView.extend({
template: '#form-template',
ui: {
text: 'textarea[name="text"]',
submit: 'button[type="submit"]'
},
events: {
'click #ui.submit': 'submitForm'
},
submitForm: function (event) {
event.preventDefault();
this.model.set({
text: this.ui.text.val()
});
this.model.save({}, {
success: function (model, response, options) {
???
}
});
}
});
Ok, my problem solution is here. In controller action "form" I create event listener
var
formView = new _FormView({
model: model
});
formView.on('formSave', function (model) {
if (id == null) {
self.list.add(model);
}
...
});
Then in form view I trigger event
this.model.save({}, {
success: function (model, response, options) {
if (response.state.success) {
self.trigger('formSave', model);
}
}
});
That's all:)
In my Marionette app, I have a Collection view, with a childView for it's models.
The collection assigned to the CollectionView is a PageableCollection from Backbone.paginator. The mode is set to infinite.
When requesting the next page like so getNextPage(), the collection is fetching data and assigning the response to the collection, overwriting the old entries, though the full version is store in collection.fullCollection. This is where I can find all entries that the CollectionView needs to render.
Marionette is being smart about collection events and will render a new childView with it's new model when a model is being added to the collection. It will also remove a childView when it's model was removed.
However, that's not quite what I want to do in this case since the collection doesn't represent my desired rendered list, collection.fullCollection is what I want to show on page.
Is there a way for my Marionette view to consider collection.fullCollection instead of collection, or is there a more appropriate pagination framework for Marionette?
Here's a fiddle with the code
For those who don't like fiddle:
App = Mn.Application.extend({});
// APP
App = new App({
start: function() {
App.routr = new App.Routr();
Backbone.history.start();
}
});
// REGION
App.Rm = new Mn.RegionManager({
regions: {
main: 'main',
buttonRegion: '.button-region'
}
});
// MODEL
App.Model = {};
App.Model.GeneralModel = Backbone.Model.extend({});
// COLLECTION
App.Collection = {};
App.Collection.All = Backbone.PageableCollection.extend({
model: App.Model.GeneralModel,
getOpts: function() {
return {
type: 'POST',
contentType: 'appplication/json',
dataType: 'json',
data: {skip: 12},
add: true
}
},
initialize: function() {
this.listenTo(Backbone.Events, 'load', (function() {
console.log('Load more entries');
// {remove: false} doesn't seem to affect the collection with Marionette
this.getNextPage();
})).bind(this)
},
mode: "infinite",
url: "https://api.github.com/repos/jashkenas/backbone/issues?state=closed",
state: {
pageSize: 5,
firstPage: 1
},
queryParams: {
page: null,
per_page: null,
totalPages: null,
totalRecords: null,
sortKey: null,
order: null
},
/*
// Enabling this will mean parseLinks don't run.
sync: function(method, model, options) {
console.log('sync');
options.contentType = 'application/json'
options.dataType = 'json'
Backbone.sync(method, model, options);
}
*/
});
// VIEWS
App.View = {};
App.View.MyItemView = Mn.ItemView.extend({
template: '#item-view'
});
App.View.Button = Mn.ItemView.extend({
template: '#button',
events: {
'click .btn': 'loadMore'
},
loadMore: function() {
Backbone.Events.trigger('load');
}
});
App.View.MyColView = Mn.CollectionView.extend({
initialize: function() {
this.listenTo(this.collection.fullCollection, "add", this.newContent);
this.collection.getFirstPage();
},
newContent: function(model, col, req) {
console.log('FullCollection length:', this.collection.fullCollection.length, 'Collection length', this.collection.length)
},
childView: App.View.MyItemView
});
// CONTROLLER
App.Ctrl = {
index: function() {
var col = new App.Collection.All();
var btn = new App.View.Button();
var colView = new App.View.MyColView({
collection: col
});
App.Rm.get('main').show(colView);
App.Rm.get('buttonRegion').show(btn);
}
};
// ROUTER
App.Routr = Mn.AppRouter.extend({
controller: App.Ctrl,
appRoutes: {
'*path': 'index'
}
});
App.start();
You could base the CollectionView off the full collection, and pass in the paged collection as a separate option:
App.View.MyColView = Mn.CollectionView.extend({
initialize: function(options) {
this.pagedCollection = options.pagedCollection;
this.pagedCollection.getFirstPage();
this.listenTo(this.collection, "add", this.newContent);
},
// ...
}
// Create the view
var colView = new App.View.MyColView({
collection: col.fullCollection,
pagedCollection: col
});
Forked fiddle
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);
}
I'm having trouble with a Backbone.js tutorial from Treehouse. Here's my code:
var NotesApp = (function () {
var App = {
stores: {}
}
App.stores.notes = new Store('notes');
// Note Model
var Note = Backbone.Model.extend({
//Local Storage
localStorage: App.stores.notes,
initialize: function () {
if (!this.get('title')) {
this.set({
title: "Note at " + Date()
})
};
if (!this.get('body')) {
this.set({
body: "No Body"
})
};
}
})
//Views
var NewFormView = Backbone.View.extend({
events: {
"submit form": "createNote"
},
createNote: function (e) {
var attrs = this.getAttributes(),
note = new Note();
note.set(attrs);
note.save();
},
getAttributes: function () {
return {
title: this.$('form [name=title]').val(),
body: this.$('form [name=body]').val()
}
}
});
window.Note = Note;
$(document).ready(function () {
App.views.new_form = new NewFormView({
el: $('#new')
});
})
return App
})();
And I get the error: Cannot set property 'new_form' of undefined
I've tried to go back and copy the code as close as possible, but I still couldn't get it to work. Any suggestions?
After stores: {} add ,
views: {}.
You need an object to attach your view to - JavaScript has no vivification