Bind to error event of a model created by collection.create()? - javascript

I have a collection of Comments and a view which is used to create new comments. Each comment has some client side validation going on:
class Designer.Models.Comment extends Backbone.Model
validate: (attrs) ->
errors = []
# require presence of the body attribte
if _.isEmpty attrs.body
errors.push {"body":["can't be blank"]}
unless _.isEmpty errors
errors
The Comments collection is super simple:
class Designer.Collections.Comments extends Backbone.Collection
model: Designer.Models.Comment
I create comments in the NewComment view. This view has access to the comments collection and uses it to create new comments. However, validations fails in the Comment model don't seem to bubble up through the collection. Is there a batter way to do this?
class Designer.Views.NewComment extends Backbone.View
events:
'submit .new_comment' : 'handleSubmit'
initialize: ->
# this is where the problem is. I'm trying to bind to error events
# in the model created by the collection
#collection.bind 'error', #handleError
handleSubmit: (e) ->
e.preventDefault()
$newComment = this.$('#comment_body')
# this does fail (doesn't hit the server) if I try to create a comment with a blank 'body'
if #collection.create { body: $newComment.val() }
$newComment.val ''
this
# this never gets called
handleError: (model, errors) =>
console.log "Error registered", args

The problem is that the collection event that aggregates all of the model events hasn't been hooked up yet. That hookup happens in the _add() function. Since the validation fails before the model gets added, you don't get the event.
The only indication of failure happens when create returns false but it looks like you've figured that out already.
If you need the validation errors, you will need to come up with a way to get the errors to you.
One way would be to fire off an EventAggregator message inside the validator. The other would be to circumvent or re-define the Collection.create function to hook the error event on the model.
Something like this?
model = new Designer.Models.Comment()
model.bind "error", #handleError
if model.set body: $newComment.val()
model.save success: -> #collection.add(model)

Related

Backbone "add" and "update" events confusion

From the Backbone.js (1.2.3) documentation:
add collection.add(models, [options])
Add a model (or an array of models) to the collection, firing an "add"
event for each model, and an "update" event afterwards.
In my code I want to add a new model (visually a card with an input) to the collection and then force focus on the input of created card. I'm not sure if it's the correct way, but I basically listen to the event fired when model is added to collection and trigger another event which facilitates focusing from the view of created model:
# ItemView
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
When I listen to the update event, it works as expected: :last card (the new model) is selected and input is focused.
But when I listen to the add event, initialize function fires on the penultimate model, instead of the new model. The new model is still created, but the edit class and focus is forced on the one before.
Why is that happens?
I would use an update event for this purpose, but unfortunately for me model.destroy method also fires an update event, so it results in ruined UI, and if I pass model.destroy with silient:true bad things happen overall. Is there a workaround?
Relevant code:
# CompositeView
class List.Models extends App.Views.CompositeView
template: "path/to/template"
childViewContainer: "div.destination"
childView: List.Model
events:
"click #add-model": "addModel"
initialize: ->
#listenTo #collection, "update", ->
App.vent.trigger "focus:field"
addModel: (e) ->
#$el.find("#add-model").prop "disabled", true
model = App.request "new:model:entity"
#collection.add(model)
# ItemView
class List.Model extends App.Views.ItemView
template: "path/to/template"
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
Edit:
Apparently the difference between add and update events is that the add event is fired immediately after #collection.add(model) is called, but before new model is inserted in the DOM, resulting in $("div.card:last") selector pointing to the penultimate view. I'm not sure however, maybe more experienced people can clarify whether this is true or not. I've come to this conclusion after adding timeout to the execution of the App.vent:
initialize: ->
delay = (ms, func) -> setTimeout func, ms
# Triggered via add event
App.vent.on "focus:field", =>
delay 100, =>
$("div.card:last").addClass("edit")
$("input:first").focus()

Why isn't my Backbone Collection reset event firing?

I went through the Railscast tutorial and got it all working. Working on a quick prototype to see if Backbone is viable but I've messed something up and I'm not sure what I've done wrong. I'm on Backbone 1.
View
class Shsh.Views.AssetsIndex extends Backbone.View
template: JST['assets/index']
initalize: ->
#collection.on('reset', #render, this)
render: ->
$(#el).html(#template(assets: #collection))
console.log('rendered')
this
Router
class Shsh.Routers.Assets extends Backbone.Router
routes:
'': 'index'
initialize: ->
#collection = new Shsh.Collections.Assets()
#collection.fetch({reset: true})
index: ->
view = new Shsh.Views.AssetsIndex(collection: #collection)
$('#container').html(view.render().el)
The view gets rendered fine, but the length of #assets comes back as 0. I can go through the steps in the console and when I render the view again it comes back as being the correct length. What am I doing wrong?
EDIT:
I also do actually have a collection and model. The code there is all boilerplate generated by Backbone On Rails.
You are calling fetch() too early -- in router creation. Should be called in specific route code instead. The way you implemented it, fetch and reset may complete before route is triggered and therefore you'll start listening to reset after it has been fired
I'm an idiot. Initialize is spelt wrong in Shsh.Views.AssetsIndex.

Backbone context issue with coffeescript

I have a simple calendar that re-renders whenever the date changes. After onClick the date is set triggering the change:date event and the render method is called.
However, the context is off as the html is not being replaced.
I don't know why this is so because I am using => to preserve the context and when I console.log #$el it always shows me the same class ie. WidgetView
It works when I do $(elementName).html but not with #$el.html. Ideas?
class WidgetView extends sandbox.mvc.View
className: 'sidebar-group'
events:
"click a" : "onClick"
template: sandbox.template.compile tmpl
initialize: (options) ->
#date = new DateModel {rid:1000}
#listenTo #date, "change:date", #render
onClick: (e) ->
e.preventDefault()
# Get value
value = $(e.currentTarget).text()
# Set date model
#date.set {date:value}
# Emit model change event
sandbox.emit "model.date.change", #date
return false
render: (model) =>
data = CalendarResource()
#$el.html #template {calendar: data}
#
I think the problem is pretty simple. Look at listenTo closely if you didn't read it.
object.listenTo(other, event, callback)
Unlike bind
object.on(event, callback, [context])
If you see listenTo doesn't set the context of your callback in any shape or form.
Try just doing
#listenTo #date, "change:date", _.bind(#render, this);
Bleh I'm not a coffee scripter but you get the point. Force the context in the constructor to always be the instance.
Problem
The issue for this particular problem was that I was using the JQuery Page Slide plugin which was copying over the element to another node.
As a result, I was updating the reference node and the plugin was not keeping the new node in sync.
Solution
Since there was no refresh method for the plugin, I will have to copy over the elements and rebind all events.

Why is backbone.js returning an empty array when accessing models?

I have a router accessing its collection. My for loop wasn't iterating through the models so I tried logging the collection to see what it returned. Turns out when I log the collection directly I see all of the models as expected. But if I try to log the models attribute of the collection I get an empty array! It doesn't make sense. These lines are directly following each other. I tried changing the order and got the same outcome.
console.log(this.collection);
=> Shots
_byCid: Object
_byId: Object
length: 15
models: Array[15]
__proto__: Shots
...
console.log(this.collection.models);
=> []
console.log(this.collection.length);
=> 0
Why would this happen?
Here is the code as it is in the router to give a better context of where this code is firing:
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
console.log #collection
console.log #collection.models
Jim,
This doesn't fix your problem - you've worked that out. But it explains why you're seeing the console output you see.
When you run console.log(this), you output the object itself and the console links references (pointers if you like) to the inner variables.
When you're looking at it in the console, at the time the console.log(this) runs the models area is empty, but at the time you look at the logs, the collection has finished loading the models and the inner array variable is updated, AND the reference to that variable in the object log shows the current content.
Basically in console.log(this),inner models variable continues its normal life and the console shows the current status at the time you're looking at it, not at the time you called it.
With console.log(this.models), the array is dumped as is, no reference is kept and all the inner values are dumped one by one..
That behaviour is quite simple to reproduce with a short timeout, see this fiddle.. http://jsfiddle.net/bendog/XVkHW/
I found that I needed to listen for the collection to reset. So instead of passing the model into the view I created another view expecting the collection and listened for the 'reset' event to fire 'render' for the view.
# Routers
class Draft.Routers.Shots extends Backbone.Router
routes:
'' : 'index'
'shots/:id' : 'show'
initialize: ->
#collection = new Draft.Collections.Shots()
#collection.fetch()
index: ->
view = new Draft.Views.Desktop(collection: #collection)
# Views
class Draft.Views.Desktop extends Backbone.View
el: $("body")
initialize: ->
#collection.on("reset",#render,this)
render: ->
console.log #collection
console.log #collection.length
You can use a promise. (.done will do fine)
#collection.fetch().done =>
for model in #collection.models
console.log model
this will give you #collection's models fetched and ready to go.
or if you don't need to force the app to wait,
#collection.on 'sync', =>
for model in #collection.models
console.log model
Both of these will let you do what you want.

backbone.js having trouble with fetch() on a collection

Warning: Code is in Coffeescript. I hope that's ok.
I have a model, Song, a collection Songs, and a view SongsView. Here it is:
SONG_TEMPLATE = '''
<table>
{{#if songs.length }}
{{#each songs}}
<tr><td>{{ this.name }}</td><td>{{ this.duration }}</td></tr>
{{/each}}
{{/if}}
</table>
'''
$ ->
class Song extends Backbone.Model
parse: (response) ->
console.log "model parsing #{response}"
#
class Songs extends Backbone.Collection
initialize: ->
#model = Song
#url = "/songs/data"
parse: (response) ->
console.log "collection parsing"
console.log response
# This works. The JSON here was grabbed right out of the XHR response I got from the server and pasted into my code.
songs = new Songs(
[
{"name":"Stray Cat Strut","rating":4,"duration":3},
{"name":"Chinatown","rating":2,"duration":4.2},
{"name":"Sultans of Swing","rating":3,"duration":5.4},
{"name":"Pride & Joy","rating":3,"duration":3}
]
)
# This fails. It should be exactly the same as the above code, and indeed, the collection parsing takes place.
# However, the view renders nothing.
# songs = new Songs
# songs.fetch()
class SongsView extends Backbone.View
initialize: ->
#model = Song
#render()
render: =>
console.log "render"
console.log #collection
template = Handlebars.compile(SONG_TEMPLATE)
#template = template(songs: #collection.toJSON())
console.log "template: #{#template}"
$('#song-list').html #template
songView = new SongsView(collection: songs)
The issue I'm having is that there is some subtle difference between initializing songs from the JSON string and allowing backbone to populate it using fetch(). The object looks ok in the script debug window, but no joy.
So, what's going on here and am I sort of on the right track?
Thanks
Fetch is an asynchronous method, this means that when you render your view, the data has not been retrieved, but when you write it manually, the data is there. The general way to do this is to bind the reset trigger that gets called by fetch to the render method.
class SongsView extends Backbone.View
initialize: ->
#model = Song
#collection.bind("reset", #render)
render: =>
console.log "render"
console.log #collection
template = Handlebars.compile(SONG_TEMPLATE)
#template = template(songs: #collection.toJSON())
console.log "template: #{#template}"
$('#song-list').html #template
You should probably more your songs.fetch below where you instantiate your view too.
As answered by Gazler, the problem is that fetch is asynchronous. If you want to use Gazler's solution, be aware that the collection's fetch method no longer triggers the reset event by default. Therefore, you'll need to have the collection explicitly trigger the reset event:
my_collection.fetch({reset: true})
Another solution to solving this is using jQuery deferreds to display the view once the results have been fetched. More one using deferreds to manage view display when fetching data asynchronously: http://davidsulc.com/blog/2013/04/01/using-jquery-promises-to-render-backbone-views-after-fetching-data/
And waiting for multiple asynchronous data sources to return: http://davidsulc.com/blog/2013/04/02/rendering-a-view-after-multiple-async-functions-return-using-promises/

Categories

Resources