How to update collection and view with new data from response? - backbone.js

How to update posts collection with new data from server in handleSliderChange()? When I'm trying to use fetch() in $.getJSON collection is reseted with old data.
define ['jquery','backbone','app','views/posts/post_view','templates/posts/index'],
($, Backbone, App, PostsView) ->
class App.Views.Posts.IndexView extends Backbone.View
template: JST["posts/index"]
events:
"slidechange #slider": "handleSliderChange"
initialize: () ->
#options.posts.on('reset', #render, #)
addAll: () ->
#options.posts.each(#addOne, this)
addOne: (post) ->
view = new PostView({model: post})
$(#el).find("#list").append(view.render().el)
slider: ->
$(#el).find("#slider").slider({})
handleSliderChange: (e, ui) ->
self = this
$.getJSON "/posts?scope="+ui.value, (data) ->
#how to update posts collection with 'data'?
render: ->
$(#el).html(#template(posts: #options.posts.toJSON()))
#slider()
#addAll()
#

Use Collection.reset to replace an existing collection with new models. Assuming that the response from "/posts?scope?ui" is an array of hashes, you would use:
$.getJSON "/posts?scope="+ui.value, (data) ->
self.collection.reset data
Note that Collection.fetch is sort of like calling "getJSON" followed by "reset", so you may want to consider using that instead.
options = {}
options["url"] = "/posts?scope="+ui.value
#collection.fetch options

Related

How to organize Backbone collection with a specific selection?

I have a collection of items. I would like to keep track of the current selection. When the user clicks on a different item in the collection, I want to indicate that the item is selected and display the details of the selected item. Think of this as a list with a detail view (like a typical email client).
Example of a master-detail layout (source):
I currently have something like this (written in CoffeeScript, templates use haml-coffee):
class Collections.Items extends Backbone.Collection
model: Models.Item
setCurrentSelection: (id)->
# what to do here? Is this even the right way to do it?
getCurrentSelection: ->
# what to do here? Is this even the right way to do it?
class Views.ItemsPage extends Backbone.View
list_template: JST['items/list']
details_template: JST['items/details']
events:
'click .item': 'updateSelection'
initialize: (options)->
#collection = options.collection
render: ->
$('#items_list').html(#list_template(collection: #collection.toJSON())) # not sure if this is how to render a collection
$('#item_details').html(#details_template(item: #collection.currentSelection().toJSON())) # how to implement currentSelection?
#
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
# mark the item as selected
# re-render using the new selection
# templates/items/list.hamlc
%ul
- for item in #collection
%li{data:{id: item.id}, class: ('selected' if item.selected?)} # TODO: How to check if selected?
= item.name
# templates/items/details.hamlc
%h2= #item.name
I'm not sure if I'm following you (my CoffeeScript is a bit rusty), but I think what you're trying to do is set a selected property on the appropriate model in your updateSelection method, and then re-render your view.
In other words:
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
model = this.collection.get(item_id) # get the model to select
model.selected = true # mark the item as selected
this.render() # re-render using the new selection
even saying "my CoffeeScript is a bit rusty" is too much for me. But i'll still attempt to explain as best as i can in js.
First the backbone way is to keep models as a representation of a REST resource document. (server side - persisted data).
Client side presentation logic should stick to views. to remember which list item is visible in in the details part is job of the that specific view. initiating change request for details view model is job of the list of items.
the ideal way is to have two separate views for list and details. (you can also go a bit more ahead and have a view for every item in the list view.
parent view
var PageView = Backbone.View.extend({
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
this.details = new ItemDetailView({
model : this.collection.at(1) //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
},
render: function() {
this.$el.html(this.template); // or this.$el.empty() if you have no template
this.renderList();
this.renderDetails();
},
renderList : function(){
this.$('#items_list').append(this.list.$el); //or any other jquery way to insert
this.list.render();
},
renderDetails : function(){
this.$('#item_details').append(this.details.$el); //or any other jquery way to insert
this.details.render();
}
});
list view
var ItemListView = Backbone.View.extend({
events : {
'click .item': 'updateSelection'
},
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
updateSelection : function(){
var selectedModel;
// a mechanism to get the selected model here - can be same as yours with getting id from data attribute
// or you can have a child view setup for each model in the collection. which will trigger an event on click.
// such event will be first captured by the collection view and thn retriggerd for page view to listen.
this.trigger('itemSelect', selectedModel);
}
});
details view
var ItemDetailView = Backbone.View.extend({
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
});
This won't persist the state through routes if you don't reuse your views. in that case you need to have a global state/event saving mechanism. somthing like following -
window.AppState = {};
_.extend(window.AppState, Backbone.Events);
//now your PageView initilize method becomes something like this -
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
var firstModel;
if(window.AppState.SelectedModelId) {
firstModel = this.collection.get(window.AppState.SelectedModelId);
} else {
firstModel = this.collection.at(1);
}
this.details = new ItemDetailView({
model : firstModel //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
window.AppState.SelectedModelId = selectedModel.id;
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
}
EDIT
Handling selected class (highlight) in list view . see comments for reference.
list view template -
<ul>
<% _.each(collection, function(item, index){ %>
<li data-id='<%= item.id %>'><%= item.name %></li>
<% }); %>
</ul>
inside list view add following method -
changeSelectedHighlight : function(id){
this.$(li).removeClass('selected');
this.$("[data-id='" + id + "']").addClass('selected');
}
simply call this method from updateSelection method and during PageView initialize.
this.list.changeSelectedHighlight(firstModel.id);

Backbone collection and view sorting

I set 'comparator' method in my collection so that I can sort list in collection.
but it doesn't effect to view.
'/api/note/getList', it returns (and it called when collection be initialized by view)
[{"id":22,"name":"Test2","isPublic":1},{"id":11,"name":"Test1","isPublic":1},{"id":33,"name":"Test3","isPublic":1}]
This is my collection,
define [
'models/Note'
],
(Note) ->
class NoteCollection extends Backbone.Collection
model : Note
url : '/api/note/getList'
initialize: () =>
_.bindAll #
#.on 'sort', #onSort
comparator : (item) ->
id = item.get 'id'
return id * -1
onSort: () =>
#.each (model) =>
console.log model.get 'id'
onSort method prints,
11
22
33
correctly, but on view, it shows
22
11
33
This is my view,
define [
'hbs!./noteCollection_tpl'
'./noteItemView'
'collections/NoteCollection'
],
(noteCollection_tpl, noteItemView, NoteCollection) ->
class NoteCollectionView extends Backbone.Marionette.CompositeView
template : noteCollection_tpl
itemView : noteItemView
itemViewContainer : '.noteListContainer'
className : 'noteWrap'
initialize : (options) ->
#collection = new NoteCollection()
Do I have to re-render view? if so, how can I catch the (what) event when after url be loaded and all list be into collection?
please advice what I am doing wrong.
Need to render when collection be sorted.
initialize : (options) ->
#collection = new NoteCollection()
#collection.on 'sort', () =>
#render()
Aww, better using 'listenTo' because,
When the view is destroyed, the listenTo call will automatically remove the event handler. This prevents memory leaks and zombie event listeners.
I'v believed, using marionette will solve this zombie event listeners.
Thanks Yurui Ray Zhang.

How to get trigger to listen properly

I am having a hard time getting my trigger to respond properly. I have plenty that are working but one of them isn't and I can't understand why.
Here is my AppController class
class ProjectOrder.View.AppController extends Backbone.View
initialize: ->
#promptOfficeSearch()
promptOfficeSearch: ->
officeSearch = new ProjectOrder.View.OfficeSearch
officeSearch.on 'createOffice', #promptOfficeCreate
officeSearch.on 'createTicket', #promptTicketCreate
officeSearch.on 'accountAndTicketExist', #killProcessAccountExists
promptOfficeCreate: (serial) ->
#officeModel = new ProjectOrder.Model.OfficeModel()
#officeModel.set('serial_number', serial)
officeCreate = new ProjectOrder.View.OfficeCreator({model: #officeModel})
officeCreate.on 'createTicketOffAccount', #promptTicketCreate
promptTicketCreate: (model) ->
console.log 'promptTicketCreate'
model = model || #officeModel
ticketModel = new ProjectOrder.Model.TicketModel()
new ProjectOrder.View.TicketCreator({ticketModel: ticketModel, officeModel: model})
killProcessAccountExists: (ticket_id) ->
msg = document.createElement 'div'
msg.className = 'account-exists-msg'
msg.innerHTML = "Account already exists. Redirecting to ticket #{ticket_id}..."
$('#create-order-div').append(msg)
setTimeout((->
window.location = "/pto/#{ticket_id}"
), 2000)
All of the triggers from the officeSearch object in the promptOfficeSearch function work properly. They are all triggered as follows, respectively:
#trigger 'createOffice', serial
#trigger 'createTicket', data.model[0]
#trigger 'accountAndTicketExist', data.model
But with the officeCreate object in the promptOfficeCreate, it does not respond to the createTicketOffAccount event which is registered in the submitOffice ajax success callback in my OfficeCreator class:
class ProjectOrder.View.OfficeCreator extends Backbone.View
template: _.template($("#OfficeCreator").html())
id: 'office-creator'
events:
'click .submit' : 'submitOffice'
initialize: ->
#render()
render: ->
#$el.html(#template(#model.toJSON()))
$('#create-order-div').append(#$el)
submitOffice: ->
#setModelData()
#model.save(null,{
success: (model) =>
#trigger 'createTicketOffAccount', model
##$el.remove()
error: ->
alert 'error'
})
setModelData: ->
#model.set({
office_name: $('#office').val()
doctor_name: $('#doctor').val()
emr: $('#has-emr').is(':checked')
forms_builder: $('#has-forms').is(':checked')
iehr: $('#has-iehr').is(':checked')
clipboard: $('#has-clip').is(':checked')
specialty_id: $('#specialty').val()
})
any ideas why my trigger is not working?
I think you need fat arrows on all the methods in your AppController class.
When this event fires:
officeSearch.on 'createOffice', #promptOfficeCreate
the promptOfficeCreate function gets invoked as a normal function as opposed to a method bound to your controller instance as this, so when this happens:
officeCreate.on 'createTicketOffAccount', #promptTicketCreate
#promptTicketCreate is undefined and the event binding doesn't wire up properly.

Backbone.js - Build view with sub models

I have an edit view for a Backbone Model that I create each time the the element is clicked on. The problem I have is that the edit view needs two Backbone collections to create the edit form (it contains two <select> lists).
The view:
MyApp.elementView = Backbone.View.extend({
events: {
'click .edit': 'editForm',
},
editForm: function(ev) {
var editView = new TimeTrack.Views.EditJob({
model: this.model
// This view needs two more collections
// for the <select> elements
});
...
}
});
Instantiate the view:
var elementView = new MyApp.elementView({
collection: elementCollection
});
What is the best way to push the needed collections to the edit view? Do I have to pass the collections need for the edit view from the elementView form the instantiation? Or is there a better way of doing this?
I did so, passed in view 2 collections, 1 - the main and the other as follows:
to elementView - second collection and in elementView recive her.
example:
in router I'm
initialize: ->
(YourNameSpace).secondCollection = new (YourNameSpace).secondCollection
elements: =>
view = new (YourNameSpace).elementView( secondCollection: #secondCollection )
$('.l-yield').html(view.render().el)

Backbone.js error - Uncaught TypeError: Object [object Object] has no method 'set'

My Code:
I am new to Backbone.js and trying to build an app with Backbone.js and PHP. When I am trying to call add in the router, I am getting error:
Uncaught TypeError: Object [object Object] has no method 'set'.
Please help me to find my mistake.
Thanks.
// Models
window.Users = Backbone.Model.extend({
urlRoot:"./bb-api/users",
defaults:{
"id":null,
"name":"",
"email":"",
"designation":""
}
});
window.UsersCollection = Backbone.Collection.extend({
model:Users,
url:"./bb-api/users"
});
// Views
window.AddUserView = Backbone.View.extend({
template:_.template($('#new-user-tpl').html()),
initialize:function(){
this.model.bind("click", this.render, this);
},
render:function(){
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events:{
"click .add":"saveUser"
},
saveUser:function(){ alert('saveUser');
this.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(this.model.isNew()){
this.model.create(this.model);
}
return false;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"welcome",
"users":"list",
"users/:id":"userDetails",
"add":"addUser"
},
addUser:function(){
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
$('#content').html(this.addUserView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
As suggested in the comments, the problem starts here here:
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
and finishes here:
saveUser:function(){ alert('saveUser');
this.model.set({
By passing a collection in place of a model you create confusion, and as a result later in the saveUser function you try to call a Backbone.Model method (set) on a Backbone.Collection instance.
Note: As of version 1.0.0 Backbone.Collection now has a set method. In previous versions, such as the one used by the question's author, that method was instead called update.
There are several steps you can take to clarify this code. For starters, I would rename your model and collection classes so that it's clear that the model is the singular form and the collection is the plural form:
window.Users => window.User
window.UsersCollection => window.Users
Next, I would create a new User model, instead of a Users collection, and pass that to your view:
this.addUserModel = new User();
this.addUserView = new AddUserView({model:this.addUserModel});
Finally, I'd remove these lines:
if(this.model.isNew()){
this.model.create(this.model);
}
For one thing, the model will always be new (as you just created it before passing it in), but more importantly you don't need to call the Collection's create method because that method creates a new model, when you already have one created. Perhaps what you should add instead is :
this.model.save();
if your intent is to save the model to your server.
Since you already specified a urlRoot for the model, that should be all you need to create a new model, pass it to your view, have your view fill in its attributes based on DOM elements, and finally save that model's attributes to your server.
I think you are facing problem with object scope. When event fired it send to event object to that function. Just try this it may work
Declare global variable with the current view inside the initialize
initialize : function(){ self = this; }
then change this to self,
saveUser:function(){ alert('saveUser');
self.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(self.model.isNew()){
self.model.create(this.model);
}
return false;
}

Resources