Weird behavior in using sortBy with Backbone/Coffeescript - backbone.js

I am using Backbone with Coffeescript. The code I use for my view is:
initialize: ->
#collection.on "reset", #render, #
#collection.fetch({reset: true})
render: ->
#collection = #collection.sortBy (item) -> item.get('name')
#collection.forEach #renderEntry, #
#
renderEntry: (model) ->
v = new App.Views.EntryView({model: model})
#$el.append(v.render().el)
The problem is when I want to sort Backbone collection on the first line of render function I get Uncaught TypeError: Object [object Array] has no method 'sortBy' error. If I change render function and rewrite it as :
render: ->
sorted = #collection.sortBy (item) -> item.get('name')
sorted.forEach #renderEntry, #
#
then everything works fine. What's wrong with original code?
I tried to move sorting functionality to another function and nothing changed. Again when I want to assign sorted collection to the collection itself I get the same error.
Any ideas?
Thanks in advance.

Solved!
The problem is that reset event gets triggered two times. One by Backbone and second time by fetch. So the first time that I call
#collection = #collection.sortBy (item) -> item.get('name')
it replaces the collection with empty array (since nothing is fetched yet), hence there is no sortBy method in it to be called in the next run.
Replacing the line with:
(#collection = #collection.sortBy (item) -> item.get('name')) if #collection.length > 0
solves the issue.

Related

Backbone collection.reset function returns array

I have the following code in backbone view:
getAccounts: function() {
var filteredCollection = this.view.collection.reset(this.view.collection.where({ AccountStatus: 'Open' }));
return filteredCollection;
}
And I assume that this code returns me collection according to doc link http://backbonejs.org/#Collection-reset
But it returns an array. What is wrong here?
The documentation says
Returns the newly-set models
This means you get an array containing newly set models. It doesn't say it returns the collection itself. There is no reason to return the collection itself because you just performed this action on the collection and you already have access to it.
You can just do return this.view.collection instead.

AngularJS array.push ng:repeat not updating

I have a create function in a service which is responsible for taking input data, turning it into a resource, doing a save and inserting it into a local cache.
create = (data, success, error) ->
throw new Error("Unable to create new user, no company id exists") unless $resource_companies.id()
# create the resource
new_model = new resource(data)
# manually add the company id into the payload (TODO: company id should be inferred from the URL)
new_model.company_id = $resource_companies.id()
# do the save and callbacks
new_model.$save( () ->
list.push(new_model)
success.call(#) if success?
, error)
My problem is that an ng:repeat that watches the list variable is not getting updated. As far as I can tell this is not occurring outside on AngularJS so it does not require a $scope.$apply() to keep it up to date (indeed, if I try to trigger a digest, I get a digest already in progress error)
What's doubly weird, is I have this exact same pattern used elsewhere without issue.
The code used by my controller to access the list array is (this is in the service)
# refreshes the users in the system
refresh = (cb) -> list = resource.query( () -> cb.call(#) if cb? )
In the controller:
$scope.users = $resource_users.refresh()
This may be because you are assigning a new reference. Angular is watching the old reference, and it never changes. Here is the where the reference is replaced:
list = resource.query( /* ... */ )
If, instead, you make the list a property of an object, angular's ng-repeat watch should be able to observe the change in the list property of the data object:
data.list = resource.query( /* ... */ )

How do I clear out an array's contents and replace with new contents of nested collection?

I have a Project model that has a nested collection of ProjectSlides. The project slides are images that I would like (to iterate over) to show which are specific to the particular project that was clicked on.
I can click on the project name in the project list and it shows the images, iterating through each one properly. If I click on another project name, I expect the array holding the first project's images (project slides) to clear out and be replaced with the project slide images of the new project I clicked on. However, the images belonging to the project slides of the first project do clear out, and the second project's project slide images are appended to the array.
Question
How do I get the array to clear out its contents when I click on another project and have the array populate with the new project's project slide images?
In my projects controller I have the following code:
Displays the list of projects:
showProjectList: (projects) ->
projectListView = #getProjectListView projects
Demo.AboutApp.Show.on "project-name:link:clicked", (project) =>
console.log project
#showProject project # passes project model that was clicked on from another view
#projectsLayout.projectListRegion.show projectListView
Get specific Project that was clicked on:
showProject: (project) ->
console.log project
console.log project.get('project_slides')
newProjectView = #getProjectDetailsView project
#projectsLayout.projectDetailsRegion.show newProjectView
console.log slides
# When I click on another project name, how can I check to see if the array exists?
# If the array exists, then set its length to 0 (to empty the previous project's nested collection),
# and then add the nested collection project slides of the other project project I clicked on?
# Is there some way to scope an array based on an event? That event would create the array, if one doesn't exist,
# or if it does exist, empty its contents and replace? I'm so lost.
project_slides = project.get('project_slides') # shows project_slides
slides = (slide for slide in project_slides) # creates the slides array, placing each member of the project slides nested collection in the array.
console.log "slides: #{slides}"
i = 0
len = slides.length
callback = ->
slide = slides[i] # gets the current iteration of the slides array
slideView = Demo.ProjectsApp.Show.Controller.getSlidesView slide # creates the view
Demo.ProjectsApp.Show.Controller.projectsLayout.projectSlidesRegion.show slideView # appends the view to the projectsSlideRegion
console.log slide
i++
i = 0 if i >= len
return
setInterval callback, 5000
slideView = #getSlidesView slides
#projectsLayout.projectSlidesRegion.show slideView
View Code:
class Show.ProjectName extends Backbone.Marionette.ItemView
template: JST["backbone/apps/projects/templates/_project_name_on_project_list"]
tagName: "li"
events:
"click a.project-link" : ->
Demo.AboutApp.Show.trigger "project-name:link:clicked", #model
triggers:
"click .project-link" : "project:link:clicked"
class Show.ProjectSlideList extends Backbone.Marionette.ItemView
template: JST["backbone/apps/projects/templates/_project_slide"]
tagName: "li"
itemViewContainer: "project-slides"
initialize: ->
console.log "ProjectSlideList View initialized"
console.log this
modelEvents:
"add" : "render"
"change" : "render"
Model & Collection:
class Entities.Project extends Backbone.Model
url: -> Routes.project_path(id)
class Entities.ProjectsCollection extends Backbone.Collection
model: Entities.Project
url: -> Routes.projects_path()
5/22/13 Final code in controller:
showProjectList: (projects) ->
projectListView = #getProjectListView projects
Demo.AboutApp.Show.on "project-name:link:clicked", (project) =>
clearInterval #timer # <-- this is important; gets rid of previous project
#showProject project
#projectsLayout.projectListRegion.show projectListView
# Project id
showProject: (project) ->
console.log project
project_slides = project.get('project_slides')
newProjectView = #getProjectDetailsView project
#projectsLayout.projectDetailsRegion.show newProjectView
slideIndex = -1
slides_length = project_slides.length
getNextSlide = ->
console.log project
console.log project_slides
slideIndex++
slide = project_slides[slideIndex]
slideIndex = 0 if slideIndex >= slides_length
console.log slide
slideView = Demo.ProjectsApp.Show.Controller.getSlidesView slide
Demo.ProjectsApp.Show.Controller.projectsLayout.projectSlidesRegion.show slideView
return
#timer = setInterval getNextSlide, 5000
I am using Rails on the backend and the rabl gem. This allows me to pass project_slides as a child collection to the parent project. In other words, project_slides is already an array of project_slide objects. I simply needed to iterate over them at an interval.
Project {cid: "c32", attributes: Object, collection: ProjectsCollection, _changing: false, attributes: Object}
detail: "first project details"
id: 1
logo: "project-icon.png"
name: "First Project Name"
problem: "The First Project's Problem Description"
project_slides: Array[4]
0: Object
avatar: "first_image.png"
caption: "first image's caption"
id: 1
project_id: 1
__proto__: Object
1: Object
2: Object
3: Object
length: 4
cid: "c32"
collection: ProjectsCollection
id: 1
When I click on a new project, marionette js takes care of zombies and populates the correct data. No need to create another slides collection when one was being passed already. Man, I couldn't see the forest for the trees.
I think you should look into storing your slides within a collection. Then, all you'd have to do is
slides.reset(project_slides)
Using a collection is especially useful in Marionette, since a collectionView will rerender itself when the collection triggers the "reset" event.
If you want to learn more about using collection views, take a look at pages 21-31 in http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf (full disclosure: I'm the author)
I am using Rails on the backend and the rabl gem. This facilitates passing project_slides as a child collection to the parent project as json. In other words, project_slides is already an array of project_slide objects. I simply needed to iterate over them at an interval. When I click on a new project, marionette js takes care of zombies and populates the correct data. No need to create another slides collection when one was being passed already. Man, I couldn't see the forest for the trees.

Backbone view's model is missing, but I don't understand why

I'm having a simple but confusing problem. I have the following piece of code:
<div id="restaurant_locations"></div>
<script type="text/javascript">
$(function() {
window.router = new Lunchhub.Routers.RestaurantLocationsRouter({
restaurantLocations: <%= #restaurant_locations.to_json.html_safe -%>
});
Backbone.history.start({pushState: true});
});
</script>
which throws this error:
Uncaught TypeError: Cannot call method 'toJSON' of undefined
If I take out the {pushState: true} part, though, and just do Backbone.history.start() with no arguments, it works just fine.
Next to the error, it says show_view.js: 19. Here's what that part of show_view.js looks like:
ShowView.prototype.template = JST["backbone/templates/restaurant_locations/show"];
ShowView.prototype.render = function() {
$(this.el).html(this.template(this.model.toJSON())); // LINE 19
Uncaught TypeError: Cannot call method 'toJSON' of undefined
return this;
}
So I guess this.model is undefined. Here's the show_view CoffeeScript:
Lunchhub.Views.RestaurantLocations ||= {}
class Lunchhub.Views.RestaurantLocations.ShowView extends Backbone.View
template: JST["backbone/templates/restaurant_locations/show"]
render: ->
$(#el).html(#template(#model.toJSON() ))
return this
If I can make #model be what it needs to be, I guess it might fix the problem. But I don't know where #model comes from or anything.
What do I need to do?
Edit: I got a little further. In the show function below, id is set to "restaurant_locations", and there's of course no member of #restaurantLocations with an id of restuarant_locations. The fact that id set set to restaurant_locations makes a certain amount of sense; the URL I'm hitting is http://localhost:3000/restaurant_locations. But it seems like it should be calling the index function, not show, if that's the URL to which I'm going.
class Lunchhub.Routers.RestaurantLocationsRouter extends Backbone.Router
initialize: (options) ->
#restaurantLocations = new Lunchhub.Collections.RestaurantLocationsCollection()
#restaurantLocations.reset options.restaurantLocations
routes:
"new" : "newRestaurantLocation"
"index" : "index"
":id/edit" : "edit"
":id" : "show"
".*" : "index"
newRestaurantLocation: ->
#view = new Lunchhub.Views.RestaurantLocations.NewView(collection: #restaurantLocations)
$("#restaurant_locations").html(#view.render().el)
index: ->
#view = new Lunchhub.Views.RestaurantLocations.IndexView(restaurantLocations: #restaurantLocations)
$("#restaurant_locations").html(#view.render().el)
show: (id) ->
restaurant_location = #restaurantLocations.get(id)
#view = new Lunchhub.Views.RestaurantLocations.ShowView(model: restaurant_location)
$("#restaurant_locations").html(#view.render().el)
edit: (id) ->
restaurant_location = #restaurantLocations.get(id)
#view = new Lunchhub.Views.RestaurantLocations.EditView(model: restaurant_location)
$("#restaurant_locations").html(#view.render().el)
id is set to "restaurant_locations", and there's of course no member of #restaurantLocations with an id of "restuarant_locations".
Sounds like you have a routing problem so let us look at your routes:
routes:
"new" : "newRestaurantLocation"
"index" : "index"
":id/edit" : "edit"
":id" : "show"
".*" : "index"
I see several problems there. First of all, routes are not regexes so ".*" doesn't match what /.*/ does (i.e. any sequence of characters), it actually matches any number of periods (i.e. /^.*$/); you can run it through _routeToRegex yourself to see what happens to '.*':
var namedParam = /:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
//...
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(namedParam, '([^\/]+)')
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
The next problem is that ":id" matches almost anything. routeToRegexp converts ":id" to /^([^/]+)$/; that regex matches any non-empty sequence of non-slashes and in particular, that will match what most of your other routes match.
So when you expect to hit '.*': 'index' from /restaurant_locations, you're actually hitting ':id': 'show' and you're getting lucky that '/new' and '/index' aren't also getting matched by ':id'. Keep in mind that the order of elements in a (Java|Coffee)Script object is implementation defined so the order that they appear in in your source code really doesn't matter; many JavaScript implementations will respect the source order but never depend on it.
Since the route order doesn't matter, you have to look at your routes keys as a simple unordered set and you--the human--must ensure that your route patterns really do match distinct things. If you do need overlapping route patterns and you need them to get matched in a specific order, then you can use the route method in your router's initialize to manually add the routes (as routing patterns or regexes) in the required order.
Executive Summary: Fix your routes so that they match distinct things.

knockout js force rebind after sorting array items

Having trouble with knockout js. But seems to me it's a bug. Maybe there are some workaround.
There is an example of sorting list here and it works. And there is another example and it doesn't. The only difference between them is version of KO.
Any help would be appreciated.
Update:
I don't know the reason but after calling splice method KO refreshes binding in some incorrect way. So the workaround I've found - force rebind array model.
The code I use to force rebinding is follows:
// newArray is ko.observableArray([...])
var original = newArray();
newArray([]);
newArray(original); // KO will rebind this array
Is there more elegant way to force rebinding?
I believe what is happening here is that Knockout 2.1 is correctly updating the list when you splice the new item into it, but the jQuery UI sortable implementation is also adding the item to the new list.
To get around this I added a 'dragged' class to the item that gets added by the sortable implementation, and then removed it once I updated the two arrays (which causes the UI update as expected).
$list
.data('ko-sort-array', array)
.sortable(config)
.bind('sortstart', function (event, ui) {
ui.item.data('ko-sort-array', array);
ui.item.data('ko-sort-index', ui.item.index());
ui.item.addClass('dragged'); // <-- add class here
})
.bind('sortupdate', function (event, ui) {
var $newList = ui.item.parent();
if($newList[0] != $list[0]){ return; }
var oldArray = ui.item.data('ko-sort-array');
var oldIndex = ui.item.data('ko-sort-index');
var newArray = $newList.data('ko-sort-array');
var newIndex = ui.item.index();
var item = oldArray.splice(oldIndex, 1)[0];
newArray.splice(newIndex, 0, item);
$list.find('.dragged').remove(); // <-- remove the item added by jQuery here
});
You can see this working here

Resources