I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).
Related
i've a Collection inside a Model as illustrated below:
var itemModel = Backbone.Model.extend({
defaults:{
name:"",
brand:"",
priceCollection:[]
}
})
There are change listeners attached to the itemModel and also change listeners attached to collection as
this.listenTo(itemModel.get('priceCollection'),'change',this.dosomething) in a view.
The problem is that the change listeners on the collection work fine as long as the parent model hasn't changed , if the model is given a set of new attributes via itemModel.set(newattributes) the event bound on itemModel.get('priceCollection') is lost.
How do i retain this event? or should i rebind this event everytime the Model is change? or Should i move the listener on the collection from the view to the Model and trigger a custom Backbone event?
It should be noted that this model is singleton
Keep in mind that Backbone assumes a Collection and Model should be mapped 1:1 to a server side resource. It makes clear assumtions on the API layout and data structures - refer to Model.url, Model.urlRoot and Collection.url.
Proposal
You said the model is a singleton. In this case I'd suggest to maintain the model and collection separately.
Since a SomeModel is not accompanied by a certain collection SomeCollection which have a tight relationship it's not necessary to relate them on an attribute level. The effort needed to establish event listeners and sync the data is only at one place.
// some controller (app main)
var model = new SomeSingletonModel();
var collection = new SomeSingletonCollection();
var view = new SomeView({
model: model,
collection: collection
});
Probably the resource that is mapped to SomeSingletonModel will deliver an array.
What are the benefits of using a collection as model attribute (that's what model.get("name") is) over using a plain array? Syncing and change events. Both are probably only necessary when a View updates the Collection's Models. When the View only renders, a Collection does not provide any benefit in many cases.
If the data of that array needs to be updated, using a Collection is probably the right choice because of Backbone's synching mechanisms.
But how to keep the collection sync with the model (you ask)?
Your controller needs to listen to the model and update the collection on sync and reset:
model.on("sync reset", function() {
// "priceCollection" is a model attribute
collection.reset(model.get("priceCollection"));
// optionally unset "priceCollection" on the model
this.unset("priceCollection", { silent: true });
});
This will initialize the collection.
Any change to the Collection's Models will then only be part of the Collection's or Model's syncing mechanisms.
Foreword
Also see my other answer which is probably the better choice when the model is a singleton.
Note the very first statement on Backbone's assumptions on the API design on that answer.
Proposals using a coupling between Model and Collection
Note: if necessary, in all these implementations the Collection's url or Model's (the Collection's Model) url/rootUrl may get (re-)defined upon sync to control the syncing.
Update internal reference on change/sync/reset
This implementation removes the model attribute and updates an object attribute with its data.
The object attribute is one Collection instance that is only reset, not recreated, upon model change.
var CustomModel = Backbone.Model.extend({
defaults: {
// defaults go here - "children" may be contained here
},
// implement constructor to act before the parent constructor is able to
// call set() (L402 in v1.3.0) with the initial values
// See https://github.com/jashkenas/backbone/blob/1.3.0/backbone.js#L402
constructor: function() {
// create children collection as object attribute - replaces model attr.
this.children = new Backbone.Collection();
// listen to changing events to catch away that attribute an update the
// object attribute
this.listenTo(this, "change:children sync reset", this.onChangeColl);
// apply original constructor
Backbone.Model.apply(this, arguments);
},
onChangeColl: function() {
// check for presence since syncing will trigger "set" and then "sync",
// the latter would then empty the collection again after it has been updated
if (this.has("children")) {
// update "children" on syncing/resetting - this will trigger "reset"
this.children.reset(this.get("children"));
// remove implicitly created model attribute
// use silent to prevent endless loop due to change upon change event
this.unset("children", { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({a: 8, children: [{ x: 50 }, { x: 89 }]});
c.url = "/dummy"
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
listening to collection events is easier to implement since there's one instance through model lifetime
Contra
code is more complex
collection data is not part syncing without further implementations
Replace on change/sync/reset
This implementation intercepts model attribute changes and replaces its data with a Collection instance that has been initialized (reset) with the raw data.
var CustomModel = Backbone.Model.extend({
defaults: {
// this is optional
children: new Backbone.Collection()
},
initialize: function() {
// listen to model attribute changing events to swap the raw data with a
// collection instance
this.listenTo(this, "change:children sync reset", this.onChangeColl);
},
onChangeColl: function() {
if (this.has("children")) {
// use silent to prevent endless loop due to change upon change event
this.set("children", new Backbone.Collection(this.get("children")), { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({ a: 8, children: [{ x: 50 }, { x: 89 }] });
c.url = "/dummy";
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
straightforward implementation
Contra
data included in sync, excluding it takes more effort
listening to the Collection impractical: since all consumers would need to unbind/bind
Note: depending on your requirements and API design you may not want children being synced to the server automatically. In this case this solution is limited. You could overwrite toJSON() of the Model but this may limit its usage for other parts of the application (like feeding the data into a view).
Inverse relation: Collection has a Model
Maybe your primary data is actually the Collection. So decorating a Collection with additional data is another approach. This implementation provides one model along the Collection that will be updated upon collection sync.
This implementation is only best suited for fetch of collection data along with attributes (e.g. fetching directory contents with attributes of the directory itself).
var CustomCollection = Backbone.Collection.extend({
initialize: function() {
// maintain decorative attributes of this collection
this.attrs = new Backbone.Model();
},
parse: function(data, opts) {
// remove "children" before setting the remainder to the Model
this.attrs.set(_.omit(data, "children"));
// return the collection content only
return data.children;
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomCollection({ a: 1, b: 2, children: [{ x: 2 }, { x: 3 }] }, { parse: true });
c.reset({ a: 9, b: 11, children: [{ x: 5 }, { x: 10 }] } , { parse: true });
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, b: 124, children: [{ x: 42 }, { x: 47 }] }) } }
c.fetch();
Pro
straight forward implementation
delegating model events is easier
Contra
collection data is not part syncing without further implementations
requires parse() to be implemented which
-- in turn requires parse: true to always be passed to reset() and set() and
-- requires parse() to be called with the collection as scope (this) (this could be circumvented by defining parse within initialize bound to this using `bind()ยด)
Given is a nested model structure like this:
Model Website
+ id
+ name
+ images[] // List of Image instances
Model Image
+ imageName
+ imageUrl
A serialised version of the response looks like:
{
"id": 4711,
"name": "Some name",
"images" [
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
}
This nested model set is persisted in a document store and is returned on request by Website.id.
There is no by-id-relation to the nested list of images, as they are persisted as a list directly in the parent model. As far as I know, the classic relations in Ext.data.Model refer to the related models via a by-id-relation.
The question is: Is there any way that I can tell the parent model to use the Image model for each of the children in it's images list?
As a first step, you can make your images data to be loaded into the model by using a field type of auto:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{name: 'images', type: 'auto'}
// ... other fields
}
});
Then:
myModel.get('images');
Should return:
[
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
From there, you should theoretically be able to implement a fully automatized solution to creates the models from this data, and -- the hardest part -- try to keep these created records and the children data in the parent model synchronized. But this is a very involved hack, and a lot of entry points in Ext code base have to be covered. As an illustration, I once tried to do that for "has one" relations, and that represent a lot of code. As a result, I never took the time to consolidate this code, and finally never used it.
I would rather advocate for a simple and local (to the model) solution. You can add a simple method to your model to get the images as records. For example:
Ext.define('My.Model', {
// ...
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Creating a store for the associated model will save you from having to play with the proxy and the reader. It also gives you an interface that is close to Ext's default one for associations.
If you need support for loading images more than once for the same parent record, you can hook on the field's convert method.
Finally, you may also need to handle client-side modifications of associated data, in order to be able to save them to the server. If your associated model allows it, you could simply use the children store's sync method (and don't forget to update the parent model's data in the sync callback!). But if your associated model isn't connected to an endpoint on the server-side, you should be able to hook on the serialize method to save the data in the associated store (as opposed to the one stored in the parent record, that won't get updated if you work with the associated store).
Here's a last example showing both:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{
name: 'images'
,type: 'auto'
// enables associated data update
,convert: function(data) {
var store = this.imageStore;
if (store) {
store.loadData(data || []);
}
return data;
}
// enables saving data from the associated store
,serialize: function(value, record) {
var store = record.imageStore,
if (store) {
// care, the proxy we want is the associated model's one
var writer = store.proxy && store.proxy.writer;
if (writer) {
return Ext.Array.map(store.getRange(), function(record) {
return writer.getRecordData(record);
});
} else {
// gross implementation, simply use the records data object
return Ext.pluck(store.getRange(), 'data');
}
} else {
return record.get('images');
}
}
}
// ... other fields
}
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Please notice that I haven't tested this code, so it might still contains some mistakes... But I hope it will be enough to give you the general idea!
I'm just starting with Backbone.js and I am having trouble with nested models and collections.
For this example, I only have a single endpoint, /vocabulary.json.
Here is a sample of what that will return:
[
{
"id": 1,
"words": [
{
"native": "hello",
"foreign": "hola"
},
{
"native": "yes",
"foreign": "si"
},
{
//... More words in this lesson
}
]
},
{
//... More lessons coming from this endpoint
}
]
It's basically of collection of lessons, and each lesson has a collection of vocabulary words.
How could I create a words collection without another url endpoint (required by collections, it seems)?
Here's what I have so far. Actually, this is a stripped down, basic version because everything I'm trying isn't working.
/entities/vocabulary.js
Entities.Vocabulary = Backbone.Model.extend({});
Entities.Vocabularies = Backbone.Collection.extend({
model: Entities.Vocabulary,
url: "/vocabulary.json"
});
// Here is where I am struggling
Entities.Vocabulary.Word = Backbone.Model.extend({
// what to do?
});
Entities.Vocabulary.Words = Backbone.Collection.extend({
// what to do?
// Need some method to go into the Entities.Vocabularies Collection, pluck a given id
// and return the "words" attribute as a new Collection to work from.
});
Perhaps, I am thinking about this completely wrong, but I am hoping I have explained my problem well enough to help you help me.
you are almost there. You can use parse method on the model where you can write up your logic of associating the words collection to the vocabulary model.. Something in these lines.
// This would be your main Model
// Set the idAttribute on it
// Use the parse method here which hits before initialize
// where you attach the words collection on each Vocabulary Model
Entities.Vocabulary = Backbone.Model.extend({
idAttribute : 'id',
parse: function (response) {
// If theresponse has wods in response
// attach it words collection to the Vocabulary Model
if (response.words) {
this.words = new Entities.Vocabulary.Words(response.words || null, {
parse: true
});
}
// Delete the words object from response as the collection is already
// created on the model
delete response.words;
return response;
}
});
// Collection of Vocabulary
Entities.Vocabularies = Backbone.Collection.extend({
model: Entities.Vocabulary,
url: "/vocabulary.json"
});
// This would be the model for Word inside a Specific Vocabulory
// Associate a idAttribute if it has one.
// Add a parse method if you have any other extra processing for this model
Entities.Vocabulary.Word = Backbone.Model.extend({
});
// Just a Collection of words for the vocabulory
Entities.Vocabulary.Words = Backbone.Collection.extend({
});
// Pass the object, and pass in the parse: true
// parameter so that parse is called before initialize
var vocabularies = new Entities.Vocabularies(navi, {
parse: true
});
// If you want to fetch a new collection again you would just do
//vocabularies.fetch({parse: true});
console.log(mainCollection);
So each model should have a words collection directly on the Vocabulary model.
Check Fiddle
I have a simple data model that looks something like this (actual code below):
model Game:
fields: id, team_1_id, team_2_id
model GameScore:
fields: id, game_id, team_1_score, team_2_score, is_final, submission_date
model SpiritScore:
fields: id, game_id, team_1_score, team_2_score
What I want seems simple. I already have code that loads Games and GameScores in bulk. I have a 'Game' instance in hand, and can call gameScores(). And I get a store, but it's empty. I have code that will dynamically load it, by placing the store into the model's hasMany definition. But what I would really like is some way to bind the Game.gameScores() call to the my existing GameScores store. Even if it used a normal filter underneath, that gives me a single record that I can bind and use in a view. (Important note: the data does not come in nested form.)
This leads to my second question. Game:GameScores is 1:many, but I only ever display the most recent one (from live score reporting). What is the general approach here? I can also manually build a filter from the game_id, but I can only bind 1 record to a view, so I don't see how I can bring that other information into a view, short of a proper hasMany relationship. Is there another way?
Any and all advice, including telling me to RTFM (with a link to the relevant manual) would be greatly appreciated! I've been pulling my hair out on this (pro bono side project) for the last week.
Cheers!
b
Ext.define('TouchMill.model.Game', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'team_1_id', 'team_2_id' ],
hasMany: {
model: 'TouchMill.model.GameScore',
name: 'gameScores',
},
},
});
Ext.define('TouchMill.model.GameScore', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', 'is_final', 'submission_date', ],
},
// belongsTo necessary? Don't think so unless I want parent func?
});
Ext.define('TouchMill.model.SpiritScore', {
extend: 'Ext.data.Model',
config: {
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', ],
},
},
I've never used touch, so I'm speaking about Ext4 here (4.2 to be precise)... And, your model definitions seem a bit broken to me (is that working with touch?). But whatever, you'll get the general idea. If my code don't work in touch, please try with Ext4.
Also, I understood that you're loading all your scores at once. If that's not the case, my solution will need to be adapted...
So, my general reasoning is the following: if you've loaded all your scores in memory, then why not use a memory proxy that uses the score store's data as the data source for the store generated for the association? I tried that and, quite to my surprise, it worked without a glitch.
To understand this, you need to know that a proxy is an independant data source, that is a proxy can be shared between multiple stores without problem. On the other hand, a store is expected to be bound to a single view or task. For example, if you bind the same store to two different grids, then filtering the first grid will affect the second as well.
And while most proxies do not "contain" their data, memory proxy do. Here's a relevant excerpt of Ext.data.proxy.Memory#read method:
resultSet = operation.resultSet = me.getReader().read(me.data)
So, enough theory, here's the proof of concept (tested in this fiddle):
// I instantiate this proxy myself in order to have a reference available
var masterScoreProxy = Ext.create('Ext.data.proxy.Memory');
Ext.define('TouchMill.model.GameScore', {
extend: 'Ext.data.Model',
fields: [ 'id', 'game_id', 'team_1_score', 'team_2_score', 'is_final', 'submission_date' ],
// I've used a remote server to ensure this all works even asynchronously
proxy: {
// configure your own
}
});
Ext.define('TouchMill.model.Game', {
extend: 'Ext.data.Model'
,fields: [ 'id', 'team_1_id', 'team_2_id' ]
,hasMany: {
model: 'TouchMill.model.GameScore'
,name: 'gameScores'
// required in order to avoid Ext autogenerating it as 'touchmill.model.game_id'
,foreignKey: 'game_id'
// needed if we don't want to have to call gameRecord.gameScores().load()
,autoLoad: true
// first part of the magic: make the generated store use my own proxy
,storeConfig: {
proxy: masterScoreProxy
}
}
});
// Just mocking a store with two games
var gameStore = Ext.create('Ext.data.Store', {
model: 'TouchMill.model.Game'
,data: [{id: 1}, {id: 2}]
,proxy: 'memory'
});
// Creating the "master" score store (that will use the model's proxy)
var scoreStore = Ext.create('Ext.data.Store', {
model: 'TouchMill.model.GameScore'
// second part's in there
,listeners: {
load: function(store, records, success) {
if (success) {
// 1. replace the data of the generated association stores' proxy
// (I must say I'm quite surprised that I didn't had to extract the data of
// every records, nor to configure a reader and all for my shared proxy...
// But hey, that works!)
masterScoreProxy.data = records;
// 2. update already generated stores
// Alternatively, you could call gameRecord.gameScores().load() individually
// before each usage of gameRecord.gameStores()
gameStore.each(function(record) {
var childStore = record.gameScoresStore;
if (childStore) {
childStore.load();
}
});
}
}
}
});
// test first load
scoreStore.load({
callback: function(records, operation, success) {
if (success) {
// and here's to prove it
gameStore.each(function(record) {
record.gameScores().each(function(score) {
console.log('Game ' + record.id + ': ' + JSON.stringify(score.data, undefined, 2));
});
});
testRefreshedData();
}
}
});
function testRefreshedData() {
// test refreshing
scoreStore.load({
callback: function(records, operation, success) {
if (success) {
console.log('--- Scores have changed ---');
gameStore.each(function(record) {
record.gameScores().each(function(score) {
console.log('Game ' + record.id + ': ' + JSON.stringify(score.data, undefined, 2));
});
});
}
}
});
}
Regarding your other questions...
If you have a 1:n for Game:Score, you've got a 1:1 for Game:MostRecentScore... So, I'd try to use that.
As for the view, there should always be a way -- even if hackish -- to access data nested in your records. The way will depend on what you're calling view here... See, for example this question.
I'm using backbone.js and backbone relational 0.5.0 with a Rails 3.2 backend. I have a Card model which has_many Notes.
Here are my JS models and collections:
Workflow.Collections.Cards = Backbone.Collection.extend({
model: Workflow.Models.Card,
url: '/cards'
});
Workflow.Models.Card = Backbone.RelationalModel.extend({
modelName : 'card',
urlRoot : '/cards',
relations: [
{
type: Backbone.HasMany,
key: 'notes',
relatedModel: 'Workflow.Models.Note',
collectionType: 'Workflow.Collections.Notes',
includeInJSON: false,
reverseRelation: {
key: 'card',
includeInJSON: 'id'
}
}]
});
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note,
url: '/cards/74/notes' // intentionally hard-coded for now
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : '/notes'
});
Normal fetching works great, but when I try fetchRelated in the console, I get an empty array:
card = new Workflow.Models.Card({id: 74}) // cool
card.fetch() // hits the sever with GET "/cards/74" - works great
card.fetchRelated('notes') // [] - didn't even try to hit the server
What's weird is that this works:
card.get('notes').fetch() // cool - GET "/cards/74/notes"
I could use that method and parse the response text, but it feels really dirty.
Anyone know what I'm missing here?
Thanks in advance, this one is really torturing me!
Stu
You should create Card with Note ids array: card = new Workflow.Models.Card({id: 74, notes: [74, 75]}); and change the url method of Notes accordingly:
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : function () {
return this.get('card').url() + '/notes';
}
});
card = new Workflow.Models.Card({id: 74, notes: [74, 75]});
card.fetchRelated('notes');
http://jsfiddle.net/theotheo/5DAzx/
I should have posted my solution a while back - there might well be a better way, but this is the convention I've gone with:
All of the following code is in the card view (which is where the notes are displayed).
First, I bind a renderNotes method to the 'reset' event on the card's notes collection:
initialize: function () {
_.bindAll(this);
this.model.get('notes').on('reset', this.renderNotes);
var self = this;
this.model.get('notes').on('add', function(addedNote, relatedCollection) {
self.renderNote(addedNote);
});
}
I also bind to the 'add' on that collection to call a singular renderNote.
The renderNotes and renderNote methods work like this:
renderNotes: function () {
if (this.model.get('notes')) {
this.model.get('notes').each(this.renderNote);
}
},
renderNote: function (note) {
var noteView = new Workflow.Views.Note({ model: note });
this.$('.notes').append(noteView.render().el);
},
Then, the last piece of the puzzle is to actually hit the server up for the card's notes (which will in turn fire the 'reset' event I bound to above). I do this in the card view's render method:
render: function () {
// render all of the eager-loaded things
this.model.get('notes').fetch();
return this;
},
As #user1248256 kindly helped me work out in the comments on my OP, the confusion was mainly in that I expected fetchRelated to pull down lazy-loaded records - that's actually not the case.
As a side-note, this view is actually a modal and be opened and closed (removed from the page). To prevent the zombie events problem described in this excellent post, I also manually unbind the events mentioned above.