Backbone.js & Marionette.js - Cannot access views from main app.js file - javascript

I cant seem to access my Views from my main app file. I'm new to backbone and marionette and I'v read through the documents but cannot find why its not working. Any help or advise?
app.js
window.App = new Marionette.Application();
App.addRegions({
gameCriteria: "#game-criteria"
});
App.myRegion.show(myView);
});
myView.js
App.module("Views", function(Views, App, Backbone, Marionette, $, _) {
var GameCriteriaTab = Backbone.Marionette.ItemView.extend({
template: JST["game-criteria-tab"],
regions: {
rulesRegion: "#rules-region"
},
onShow: function() {
this.rulesRegion.show(RulesView);
}
});
});

did you instatiate your view before calling show ? i am waiting for a code similar to that :
App.myRegion.show(new GameCriteriaTab ({
model: new GameCriteriaModel()
}));
can you provide us with the error in chrome console ?

Not sure what you are trying to do here. I'm assuming that you want to create an app called 'windowApp', and you are trying to display a view called 'GameCriteriaTab' inside a region called 'gameCriteria'. I'm refactoring your code to the following:-
windowApp = new Backbone.Marionette.Application();
windowApp.addRegions({
gameCriteria: "#game-criteria"
});
windowApp.GameCriteriaTab = Marionette.ItemView.extend({ //Defining the view
template: " <include your template Id or className here> "
});
windowApp.on("start",function(){
var myView = new windowApp.GameCriteriaTab(); //You need to create an instance of the view if you want to render it in the page
windowApp.gameCriteria.show(myView); //Showing the view in the specified region
});
windowApp.start(); //This will run the marionette application
You cannot define any regions inside ItemView. Are you trying to create a nested view? In that case, a LayoutView would suit your needs, and you can add regions inside it. Just change the ItemView to a LayoutView then.

Related

Using Backbone.Marionette, why can I not reference #ui elements when creating a new instance of a view?

I have created a simple view, MyView, that extends from ItemView. Then when I create the instance of MyView, I am trying to add references to UI elements within the view as well as events that use those UI elements.
HTML
<div id="container"></div>
<script type="text/template" id="my-template">
<p>This is a rendered template.</p>
<button data-ui="changeModelNameButton">Change Model Name</button>
</script>
JS
// Define a custom view that extends off of ItemView
var MyView = Marionette.ItemView.extend({
template: "#my-template"
});
// Instantiate the custom view we defined above
var view = new MyView({
el: "#container",
ui: {
changeModelNameButton: '[data-ui~=changeModelNameButton]'
},
events: {
'click #ui.changeModelNameButton': function() {
alert('here');
}
}
});
// Render the view in the element defined within the custom view instantiation method
view.render();
I am receiving the following error in the console:
Uncaught TypeError: Cannot read property 'changeModelNameButton' of
undefined
I noticed that if I move the ui declarations to the view definition, it works fine, but I would like to know why I can't add those when I create the instance. Is there no way to add them to the instance or am I missing something?
Note: I am using Backbone 1.3.3, Marionette 2.4.4, Underscore 1.8.3, and jQuery 3.1.1
Options overriding view's properties
Not all the options are automatically overriding the view class properties when passed on instantiation.
ui looks like it's not one of them.
Backbone will automatically apply the following (private) viewOptions on a view:
// List of view options to be set as properties.
var viewOptions = [
'model',
'collection',
'el',
'id',
'attributes',
'className',
'tagName',
'events'
];
In the view constructor, this is extended with the chosen options (source):
_.extend(this, _.pick(options, viewOptions));
How to pass the ui option?
You either need to put the ui hash in the view class definition, or to apply the ui hash yourself.
var MyView = Marionette.ItemView.extend({
template: "#my-template",
initialize: function(options) {
// merge the received options with the default `ui` of this view.
this.ui = _.extend({}, this.ui, this.options.ui);
}
});
Note that options passed to a view are available in this.options automatically.
What's the real goal behind this?
If you're going to mess around the ui and the events with its callbacks, it would be best to define a new view.
It looks like a XY problem where the real problem lies in the architecture of the app, but we can't help since you're asking about what's blocking you right now.

Backbone.js single collection item view

I've recently started building my first Backbone.js project and am having a difficult time grasping how to handle "individual views" for single items.
If for example, I had a list of todos that were being displayed by a Backbone collection. If I wanted to have the application provide a link and view to an individual todo item, how would I go about that? Would I create a new type of collection and somehow filter the collection down by the id of an individual item? Where would this logic live? Within a router?
Edit
I've since updated my router as such:
var Backbone = require('backbone');
var IndexListing = require('./indexlisting');
var BookView = require('./bookview');
var LibraryCollection = require('./library');
module.exports = Backbone.Router.extend({
routes: {
'': 'allBooks',
'book/:id': 'singleBook'
},
allBooks: function(){
new IndexListing({
el: '#main',
collection: LibraryCollection
}).render();
},
singleBook: function(bookID){
console.log(bookID);
new BookView({
el: '#main',
collection: LibraryCollection.get(bookID)
}).render();
}
});
The bookID is coming straight from my MongoDB ID and returns within the console.log but when I attempt to run this in the browser I get the following error: Uncaught TypeError: undefined is not a function which is referring to the line collection: LibraryCollection.get(bookID)
My LibraryCollection (./library) contains the following:
'use strict';
var Backbone = require('backbone');
var Book = require('./book');
module.exports = Backbone.Collection.extend({
model: Book,
url: '/api/library'
});
Both of my endpoints for GET requests return the following:
/api/library/
[{"_id":"54a38070bdecad02c72a6ff4","name":"Book Name One"},{"_id":"54a38070bdecad02c72a6ff5","name":"Book Name Two"},{"_id":"54a38070bdecad02c72a6ff6","name":"Book Name Three"},{"_id":"54a38070bdecad02c72a6ff7","name":"Book Name Four"}]
/api/library/54a38070bdecad02c72a6ff4
{"_id":"54a38070bdecad02c72a6ff4","name":"Book Name One"}
Edit no 2
I've updated my router for singleBook as follows:
singleBook: function(id){
var lib = new LibraryCollection;
lib.fetch({
success: function(){
book = lib.get(id);
new BookView({
el: '#main',
model: book
}).render();
}
});
}
I really recommend the codeschool backbone courses here for an intro to backbone.
You would have a collection of models (todos). Each todo would have its own view and would show the model data, i.e the todo description. You would probably want to have a wrapper view also known as a collection view that would have each view as a subview, but it might be better to leave that out while you're getting started.
With regards to the router logic, try and think of the functions in a router as controller actions (assuming you are familiar with MVC). There are a number of backbone extensions that create MVC controller-like functionality.
For your question you would create the link in the view when you render it with a template engine. A few engines that come to mind are Underscore, Handlebars and Mustache. A template might look like this:
{{ description }}
In this template we pass in the model and between the curly braces you can see we are trying to pull out the id and description of that model. Out model attributes might look like this:
{
id: 1,
description: "Go shopping"
}
If you want to just get one model you need to make sure that the model has urlRoot property set. Then you need to fetch the model like this:
var singleTodo = new Todo({id: 1});
singleTodo.fetch();
As I said before I would really recommend watching a number of good tutorials on backbone before you get too deep into your app. Good luck!
I guess you mean you're trying to navigate to a page that shows a single todo item? if yes the logic would live in the router
var TodosRouter = Backbone.Router.extend({
routes: {
"todo": "allTodos"
"todo/:todoId": "singleTodo"
},
allTodos: function() {
// assume you have a todo listing view named TodoListing, extended from Backbone.View
// which the view inside has the link to click and navigate to #todo/<todoId>
var view = new TodoListing({
el: $('body'),
collection: YourTodosCollection
}).render();
},
singleTodo: function(todoId) {
// assume you have a todo single view named TodoView, extended from Backbone.View
var view = new TodoView({
el: $('body'),
model: YourTodosCollection.get(todoId)
}).render();
}
});
update based on edit 2
Yes this would works but it will need to fetch the whole collection from the DB and then only get the single model to display, which might cost a lot of the network resource/ browser memory performance.
In my point of view is good to just pass the ID of the book to the BookView, and then we fetch single book from there (provided we have a REST GET call for the single book with ID as param).
var BookView = Backbone.View.extend({
render: function () {...},
initialize: function(option) {
this.model.fetch(); // fetch only single book, with the bookId passed below
//alternative - we do not pass the model option at below, we could parse the bookId from the router's since it will be part of param in URL eg: http://xxxx/book/:bookId, then we can do the below
this.model = new BookModel({id: parsedFromRouter});
this.model.fetch();
}
});
new BookView({
el: '#main',
model: new BookModel({id: bookId})
}).render();
the fetch call should be async, so we might want to show some loading indicator and only render the view when the fetch done
this.model.fetch({
success: function () { //call render }
})

Marionette - Relationships between Application and Module

We are currently building a Marionette based application.
Basically, we have a Marionette Application that has multiple regions defined on it.
Each region will act as a container for different Modules to display their views. I want each Module to have full control of what is being displayed in it's container, but I want the Application to allocate these regions. For simplicity, let's say that each module just has a simple ItemView.
I'm considering 2 approaches to populating those regions with the module views.
The first approach says that when each module is initialized, it will create its view and it will call the application to display its view in the specified region, for example:
var app = new Marionette.Application();
app.addRegions({
regionA: "#regionA",
regionB: "#regionB"
});
app.module("moduleA", function(moduleA, app, ...){
moduleA.on("start", function(){
var viewA = new MyViewA();
app.regionA.show(viewA);
}
});
app.module("moduleB", function(moduleB, app, ...){
moduleB.on("start", function(){
var viewB = new MyViewB();
app.regionB.show(viewB);
}
});
The second approach says that each module should expose some function that returns its view. The Application will call that function when ready and it will stick the view in the designated region.
I'm not sure which approach is better and would be happy to hear opinions.
I would definitely go with the second approach, after having gone with the first approach in the past I am now hitting the limitations of this approach and moving to the second approach. I wrote a blog post about it here
It depends which approach you take, both are fine, we choose the second option because we use require.js to load our modules dynamically.
var dashboardPage = Backbone.Marionette.Layout.extend({
template: Handlebars.compile(tmpl),
regions: {
graphWidget : "#graphWidget",
datePickerWidget: "#datePickerWidget",
searchWidget : "#searchWidget"
},
widget: {
graphWidget: null,
datePickerWidget: null,
searchWidget: null,
},
initialize: function(options){
this.someId= options.someId;
//if have someId ID - fetch model;
if(this.someId){
//fetch model if campaignId not null
this.modelAjax = this.model.fetch();
}
onShow: function() {
var that = this;
if(this.modelAjax){
this.modelAjax.done(function(){
that.widget.graphWidget= new graphWidget(graphWidgetOptions);
that.listenTo(that.widget.graphWidget, 'graphWidget', that.getGraphWidgetData, that);
....
that.graphWidget.show(that.widget.graphWidget);
that.datePickerWidget.show(that.widget.datePickerWidget);

Referencing Backbone views from app view

Hey all I am pretty new to Backbone though I have put several days into trying to get familiar with this framework, and it seems everytime I start feeling comfortable I run across a new problem.
I am wondering how to reference a view that is rendered from within my main appview. I know this is a really simple issue but I just can't seem to figure it out.
So for instance I have a simple view
var SubView = Backbone.View.extend({
//something here including render function
});
Then I render that view from within the main app view
var myApp = Backbone.View.extend({
render: function{
var mysubView = new SubView();
mysubView.render();
},
editSomething: function{
mysubView.remove();
}
});
When I try and reference that view from a function (editSomething:) in the main app view, I get a reference error.
What I am trying to achieve is that I have two views that include forms. I want to swtich between the two forms as an edit function is called and when an add function is called. But I can't seem to access the views that have already been rendered.
I don't want to initialize and render a new view before removing the existing view because from what I understand, I will start to get a bunch of views floating in memory.
Reference it using this:
var myApp = Backbone.View.extend({
render: function{
this.subView = new SubView();
this.subView.render();
},
editSomething: function{
this.subView.remove();
}
});

Binding a Backbone Model to a Marionette ItemView - blocking .fetch()?

This is a 2 part question. 1) Is there a better way to render a model to a view asynchronously? I'm currently making the ajax request using the fetch method in the model (though I'm calling it explicitly upon initilization), then rendering the templated view using an application event, vent, which gets published from inside the model after the parse method is called. Cool but wonky? 2) Would a blocking fetch method be of use, and is it possible?
The application renders this to the page:
layout
navbar
index
Then it fetches the model and renders this:
layout
navbar
thing
1
something
somethingelse
But, if I don't use the vent trigger, it (expectedly) renders:
layout
navbar
thing
1
null
null
The html templates:
<!-- Region: NavBar -->
<script type="text/template" id="template-navbar">
<div id="navbar">
navbar
</div>
</script>
<!-- View: IndexView -->
<script type="text/template" id="template-index">
<div id="index">
index
</div>
</script>
<!-- View: ThingView -->
<script type="text/template" id="template-thing">
<div id="thing">
thing<br/>
<%= id %><br/>
<%= valOne %><br/>
<%= valTwo %><br/>
</div>
</script>
<!-- Region -->
<div id="default-region">
<!-- Layout -->
<script type="text/template" id="template-default">
layout
<div id="region-navbar">
</div>
<div id="region-content">
</div>
</script>
</div>
app.js:
window.App = { }
# Region
class RegionContainer extends Backbone.Marionette.Region
el: '#default-region'
# Called on the region when the view has been rendered
onShow: (view) ->
console.log 'onShow RegionContainer'
App.RegionContainer = RegionContainer
# Layout
class DefaultLayout extends Backbone.Marionette.Layout
template: '#template-default'
regions:
navbarRegion: '#region-navbar'
contentRegion: '#region-content'
onShow: (view) ->
console.log 'onShow DefaultLayout'
App.DefaultLayout = DefaultLayout
# NavBar (View)
class NavBar extends Backbone.Marionette.ItemView
template: '#template-navbar'
initialize: () ->
console.log 'init App.NavBar'
App.NavBar = NavBar
# Index View
class IndexView extends Backbone.Marionette.ItemView
template: '#template-index'
initialize: () ->
console.log 'init App.IndexView'
App.IndexView = IndexView
# Thing View
class ThingView extends Backbone.Marionette.ItemView
template: '#template-thing'
model: null
initialize: () ->
console.log 'init App.ThingView'
events:
'click .test_button button': 'doSomething'
doSomething: () ->
console.log 'ItemView event -> doSomething()'
App.ThingView = ThingView
# Thing Model
class Thing extends Backbone.Model
defaults:
id: null
valOne: null
valTwo: null
url: () ->
'/thing/' + #attributes.id
initialize: (item) ->
console.log 'init App.Thing'
#fetch()
parse: (resp, xhr) ->
console.log 'parse response: ' + JSON.stringify resp
# resp: {"id":"1","valOne":"something","valTwo":"somethingelse"}
#attributes.id = resp.id
#attributes.valOne = resp.valOne
#attributes.valTwo = resp.valTwo
console.log 'Thing: ' + JSON.stringify #
#
App.MyApp.vent.trigger 'thingisdone'
App.Thing = Thing
# App
$ ->
# Create application, allow for global access
MyApp = new Backbone.Marionette.Application()
App.MyApp = MyApp
# RegionContainer
regionContainer = new App.RegionContainer
# DefaultLayout
defaultLayout = new App.DefaultLayout
regionContainer.show defaultLayout
# Views
navBarView = new App.NavBar
indexView = new App.IndexView
# Show defaults
defaultLayout.navbarRegion.show navBarView
defaultLayout.contentRegion.show indexView
# Allow for global access
App.defaultRegion = regionContainer
App.defaultLayout = defaultLayout
# Set default data for MyQpp (can't be empty?)
data =
that: 'this'
# On application init...
App.MyApp.addInitializer (data) ->
console.log 'init App.MyApp'
# Test
App.modelViewTrigger = ->
console.log 'trigger ajax request via model, render view'
App.MyApp.vent.trigger 'show:thing'
App.timeoutInit = ->
console.log 'init timeout'
setTimeout 'App.modelViewTrigger()', 2000
App.timeoutInit()
# Event pub/sub handling
App.MyApp.vent.on 'show:thing', ->
console.log 'received message -> show:thing'
thing = new App.Thing(id: '1')
App.thingView = new App.ThingView(model: thing)
# I should be able to do this, but it renders null
# App.defaultLayout.contentRegion.show App.thingView
# Testing to see if I could pub from inside model..yes!
App.MyApp.vent.on 'thingisdone', ->
console.log 'received message -> thingisdone'
App.defaultLayout.contentRegion.show App.thingView
MyApp.start data
From a very basic standpoint, throwing aside the specific example that you've provided, here is how I would approach the problem and solution.
A Generic Problem / Solution
Here's a generic version of the problem:
You need to fetch a model by its id.
You need a view to render after the model has been fetched.
This is fairly simple. Attach the model to the view before fetching the data, then use the "sync" event of the model to render the view:
MyView = Backbone.View.extend({
initialize: function(){
this.model.on("sync", this.render, this);
},
render: function(){ ... }
});
myModel = new MyModel({id: someId});
new MyView({
model: myModel
});
myModel.fetch();
Things to note:
I'm setting up the model with its id, and the view with the model before calling fetch on the model. This is needed in order to prevent a race condition between loading the data and rendering the view.
I've specified generic Backbone stuff here. Marionette will generally work the same, but do the rendering for you.
Your Specific Needs
Blocking Fetch
Bad idea, all around. Don't try it.
A blocking fetch will make your application completely unresponsive until the data has returned from the server. This will manifest itself as an application that performs poorly and freezes any time the user tries to do anything.
The key to not doing this is taking advantage of events and ensuring that your events are configured before you actually make the asynchronous call, as shown in my generic example.
And don't call the fetch from within the model's initializer. That's asking for trouble as you won't be able to set up any views or events before the fetch happens. I'm pretty sure this will solve the majority of the problems you're having with the asynchronous call.
Events Between View And Model
First, I would avoid using MyApp.vent to communicate between the model and the view instance. The view already has a reference to the model, so they should communicate directly with each other.
In other words, the model should directly trigger the event and the view should listen to the event on the model. This works in the same way as my simple example, but you can have your model trigger any event you want at any time.
I would also be sure to the use bindTo feature of Marionette's views, to assist in cleaning up the events when the view is closed.
MyView = Backbone.Marionette.ItemView.extend({
initialize: function(){
this.bindTo(this.model, "do:something", this.render, this);
}
});
MyModel = Backbone.Model.extend({
doSomething: function(){
this.trigger('do:something');
}
});
myModel = new MyModel();
new MyView({
model: myModel
});
myModel.doSomething();
Other Items
There are some other items that I think are causing some problems, or leading toward odd situations that will cause problems.
For example, you have too much happening in the DOMReady event: $ ->
It's not that you have too much code being executed from this event, but you have too much code defined within this event. You should not have to do anything more than this:
$ ->
App.MyApp.start(data)
Don't define your Marionette.Application object in this event callback, either. This should be defined on its own, so that you can set up your initializers outside of the DOMReady callback, and then trigger them with the app.start() call.
Take a look at the BBCloneMail sample application for an example on rendering a layout and then populating its regions after loading data and external templates:
source: https://github.com/derickbailey/bbclonemail
live app: http://bbclonemail.heroku.com/
I don't think I'm directly answering your questions the way you might want, but the ideas that I'm presenting should lead you to the answer that you need. I hope it helps at least. :)
See Derick's new suggestion to tackle this common problem at: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported
In short, move the asynchronous code away from your views, which means you need to provide them with models whose data has already been fetched. From the example in Marionette's upgrade guide:
Marionette.Controller.extend({
showById: function(id){
var model = new MyModel({
id: id
});
var promise = model.fetch();
$.when(promise).then(_.bind(this.showIt, this));
},
showIt: function(model){
var view = new MyView({
model: model
});
MyApp.myRegion.show(view);
}
});

Categories