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

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);
}
});

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.

Chaplin/Backbone issue - "Add" event not fired when adding item to the collection

I'm building a test application for myself, to learn more about coffeescript, Backbone, Brunch.io and Chaplin JS, but I'm stuck and I can't figure out what I'm doing wrong.
This is my code in the todo-view.coffee:
View = require 'views/base/view'
TodoItemView = require 'views/todo/todo-item'
TodoItemModel = require 'models/todo/todo-item-model'
TodoItemCollection = require 'models/todo/todo-collection'
# Site view is a top-level view which is bound to body.
module.exports = class TodoView extends View
# Template data stuff
container: 'todo-container'
tagName: 'ul'
className: 'todo-list'
template: require './templates/todo'
# Create a custom initialize method
initialize: ->
super
# Create a new Backbone Collection with some dummy data to store
#collection = new TodoItemCollection()
# Store the dummy data in the collection
data = ["working", "studying", "gym", "sleep"]
for todoItem in data
#collection.add( new TodoItemModel({ item: todoItem }) )
# Render the view
#render()
# Listen to data events
listen:
"add collection": "renderTodoList"
# When the template is initialized we need to load
# all the list items and append them to the container
renderTodoList: ->
# Loop over all the items
for model in #collection.models
todoItemView = new TodoItemView({ container: #$el, model: model })
The problem is: the event listener ( set in the listener object ) isn't triggered. So the #renderTodoList isn't called.
Calling the #renderTodoList directly from the #initialize function does work though. But I want the function to be triggered by the "add" event on the collection.
I've also tried to trigger the event manually by adding #collection.trigger "add" in the loop that creates the new data models. But this didn't work neither.
Any ideas what I'm overseeing or doing wrong?
Stefan,
I had similar issues when I tried to use the listen hash for events. I opted to setup the listener as such in the initialize method of the view.
#listenTo #Collection, 'add', #renderTodoList, #
-Hans
#stefan and #hans,
This solved your problem for no doubt, but you guys are not utilizing the power of chaplin collection view. It by default handles any modification to the collection. If you add/remove/change a new model it re-render itself, no need to force.
find the Chaplin doc here

Marionette.js ItemView - Put child view in region

I have the following ItemView template which is filled with customer data (firstName, lastName) and I want to add a CollectionView into the div .addresses.
Template
<script type="text/html" id="template-customer-details">
<h4><%= firstName %> <%= lastName %></h4>
<button class="edit">Edit</button>
<h5>Addresses</h5>
<div class="addresses">...</div>
</script>
Layout
Layout.Details = Backbone.Marionette.ItemView.extend({
template: '#template-customer-details',
regions: {
addresses: ".addresses"
},
serializeData: function () {
return this.model.attributes;
},
initialize: function () {
this.addressList = new App.Models.AddressList();
// Error!
this.regions.addresses.show(this.addressList);
this.bindTo(this, "render", this.$el.refresh, this.$el);
this.model.bind("change", this.render.bind(this));
}
});
I am getting the error "Uncaught TypeError: Object .addresses has no method 'show'."
Do I have to wait until the view is loaded?
I think you've got things a bit confused. An ItemView doesn't do anything with a regions property (you may be thinking of the Application class), so when you try to call this.regions.addresses.show that's the same as calling ".addresses".show.
I think you probably want to use a CompositeView in this case as it combines an ItemView (which you can use for your customer data) and a CollectionView which you can use for your AddressList. You'll also need to define a separate ItemView for an address (as a CollectionView just creates an ItemView for each item in a collection).
Something a little like this (which I've not tested, so may not be entirely correct):
AddressView = Backbone.Marionette.ItemView.extend({
template: '#addressTemplate'
});
Layout.Details = Backbone.Marionette.CompositeView.extend({
template: '#template-customer-details',
itemView: AddressView,
itemViewContainer: '.addresses'
});
// Then create your view something like this:
new Layout.Details({
model: new App.Models.CustomerDetails(),
collection: new App.Models.AddressList()
});
I also don't think you need to specifically bind the change & render events like in your example as marionette will usually take care of that (the same with your implementation of serializeData, which looks like it's vaguely the same as the default implementation)

Backbone.js: models not appearing

I want to display a simple list of languages.
class Language extends Backbone.Model
defaults:
id: 1
language: 'N/A'
class LanguageList extends Backbone.Collection
model: Language
url: '/languages'
languages = new LanguageList
class LanguageListView extends Backbone.View
el: $ '#here'
initialize: ->
_.bindAll #
#render()
render: ->
languages.fetch()
console.log languages.models
list_view = new LanguageListView
languages.models appears empty, although I checked that the request came in and languages were fetched. Am I missing something?
Thanks.
The fetch call is asynchronous:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, resetting the collection when they arrive. The options hash takes success and error callbacks which will be passed (collection, response) as arguments. When the model data returns from the server, the collection will reset.
The result is that your console.log languages.models is getting called before the languages.fetch() call has gotten anything back from the server.
So your render should look more like this:
render: ->
languages.fetch
success: -> console.log languages.models
# # Render should always return #
That should get you something on the console.
It would make more sense to call languages.fetch in initialize and bind #render to the collection's reset event; then you could put things on the page when the collection is ready.
Also, _.bindAll # is rarely needed with CoffeeScript. You should create the relevant methods with => instead.

How to determine if a view has been rendered? javascript

I am creating my application using backbone.js
As seen below I have a layoutView which I use to render the layout and also a mini profile within the layout.
The issue I have is with timing. I need to have the 'render' method complete first before triggering 'renderProfile' method. How can I do that?
Onethingaday.Views.Home ||= {}
class Onethingaday.Views.Home.LayoutView extends Backbone.View
template: JST["backbone/templates/home/layout"]
initialize: ->
#options.user.bind('change',#render,#renderProfile, #)
renderProfile: ->
view = new Onethingaday.Views.Shared.MiniProfileView
user: #options.user
#$('.profile').html view.render().el
render: ->
$(#el).html(#template())
#
Your situation is why I wrote LayoutManager for Backbone, http://github.com/tbranyen/backbone.layoutmanager.
What you should be doing is separating your sub views from your main (layout) view.
So in your route callback you'd have something like this:
// Initialize a Layout View
var profile = new Onethingaday.Views.Home.LayoutView();
// Initialize a MiniProfile View
var miniProfile = new Onethingaday.Views.Shared.MiniProfileView({
model: user
});
// This appears synchronous in your code, so this should work fine
$(profile.render().el).find(".profile").html(miniProfile.render());
I would implore you to investigate my library, as I think the declarative manner in which sub views are associated to layouts is really quite elegant.

Categories