Backbone - Structuring views for a simple app - backbone.js

I'm just starting with Backbone.js so please excuse the simplicity of the question.
I'm working through the standard "Todo" example and want to extend the "Todo" so that it can have multiple fields. Currently the "Todo" app just uses a single field from within the AppView to trigger new items into the collection.
Index.html
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
App.js
app.AppView = Backbone.View.extend({
events: {
'keypress #new-todo': 'createOnEnter'
Therefore I believe the current structure is
AppView
-->Collection (Todos)
-->View (Todo List Item)
I would like to make the new item template its own view
AppView
-->Collection (Todos)
-->View (Todo List Item)
-->View (Todo : New Item)
I'm a little lost as to how this view add somethings into the collection. The appview currently just calls.
createOnEnter: function( e ) {
if ( e.which !== ENTER_KEY || !this.$input.val().trim() ) {
return;
}
app.Todos.create( this.newAttributes() );
this.$input.val('');
}
How do I get a reference to the collection from within my new view?

So simple when you know what to look for...
var view = new app.NewTodo({ collection : app.Todos});
This can then referenced inside your view using..
this.collection.create({ title: 'Bonjour', order: 99, completed: false });

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

How to get the underlying Backbone Collection in a Knockback CollectionObservable from the $data context object with nested templates

Taking the following code snippet as a quick example:
var Animal = Backbone.Model.extend();
var Zoo = Backbone.Collection.extend({ model: Animal });
var tiger = new Animal({ name: "tiger" });
var zoo = new Zoo(tiger);
var viewModel = {
tiger: kb.viewModel(tiger);
zoo: kb.collectionObservable(zoo);
}
ko.applyBindings(viewModel);
from the $data context you can get a reference to the tiger model:
tiger === $data.tiger().__kb.object;
or
tiger === $data.zoo()[0].__kb.object;
and I assume it exists somewhere on this dependantObservable function, but I can't seem to find the reference to the original Backbone Collection
$data.zoo
Does anyone have any idea of how to get at the original Backbone Collection?
Also, bonus points if you can tell me of any way to get at the Backbone Collection if the viewmodel is this instead:
viewModel = kb.collectionObservable(zoo)
the challenge here is that $data contains the results of the evaluated dependantObservable function.
EDIT
After receiving a perfectly valid answer to the question above I realized that my problem only occurs in my more complicated binding with nested templates:
The templates look like this:
<!-- outer template -->
<script type="text/html" id="tmpl-outer">
<button data-bind="click: $root.outerContext">Outer Context</button>
<div data-bind="template: { name: 'tmpl-inner', data: collection }"></div>
</script>
<!-- inner template -->
<script type="text/html" id="tmpl-inner">
<button data-bind="click: $root.innerContext">Inner Context</button>
<div data-bind="foreach: $data">
<button data-bind="click: $root.modelContext">Model Context</button>
</div>
</script>
Model and View-Model:
var model = new Backbone.Model();
var collection = new Backbone.Collection(model);
var viewModel = {
collection: kb.collectionObservable(collection),
outerContext: function (data) {
console.log(data.collection.collection() === collection);
},
innerContext: function (data) {
console.log("??????? === collection");
},
modelContext: function (data) {
console.log(data.model() === model);
}
};
ko.applyBindings(viewModel);
And finally, somewhere to render everything:
<body>
<div data-bind="template: { name: 'tmpl-outer' }"></div>
</body>
So, my initial question that I over-simplified my example for should have been: how do I get at the underlying collection on the line:
console.log("??????? === collection");
It appears that the collection in this context has been converted to a simple KnockOut observable array - there doesn't seem to be any of the important KnockBack properties.
You can get the underlying collection / model by using the getters on instances of kb.CollectionObservable and kb.ViewModel.
var collection = new Backbone.Collection(),
view_models = kb.collectionObservable(collection),
reference = view_models.collection();
console.log(collection === reference);
You can do the same with instances of kb.viewModel
var model = new Backbone.Model({ id : 1 }),
view_model = kb.viewModel(model),
reference = view_model.model();
console.log(model === reference);
You can access the collection/model as well from $data by calling the getters in the data-binds, though I really can't see any need at all to do this if you use factory view_models for the collection allowing you to define any number of specific computeds / observables for each vm.
var model = new Backbone.Model({ id : 1 });
var collection = new Backbone.Collection(model);
var AnimalViewModel = kb.ViewModel.extend({
constructor: function(model) {
kb.ViewModel.prototype.constructor.call(this, model, {});
return this;
// Custom code per vm created
}
});
var view_model = {
zoo : kb.collectionObservable(collection, {
view_model : AnimalViewModel
});
}
In the end I found that I had to go via the parent to get the collection. I don't like this level of indirection, but I can't find any way around it.
The view-model now has this function in it:
doSomethingWithUnderlyingCollection: function(collectionName, parentContext) {
var underlyingCollection = parentContext.model().get(collectionName);
// do something with the underlying collection here, e.g. add a model.
}
And then to call the method from the template:
<button data-bind="click: function() { $root.doSomethingWithUnderlyingCollection('MyCollection', $parent); }">Add</button>

How to get a single item from a GoInstant collection?

How do you get a single item from a GoInstant GoAngular collection? I am trying to create a typical show or edit screen for a single task, but I cannot get any of the task's data to appear.
Here is my AngularJS controller:
.controller('TaskCtrl', function($scope, $stateParams, $goKey) {
$scope.tasks = $goKey('tasks').$sync();
$scope.tasks.$on('ready', function() {
$scope.task = $scope.tasks.$key($stateParams.taskId);
//$scope.task = $scope.tasks.$key('id-146b1c09a84-000-0'); //I tried this too
});
});
And here is the corresponding AngularJS template:
<div class="card">
<ul class="table-view">
<li class="table-view-cell"><h4>{{ task.name }}</h4></li>
</ul>
</div>
Nothing is rendered with {{ task.name }} or by referencing any of the task's properties. Any help will be greatly appreciated.
You might handle these tasks: (a) retrieving a single item from a collection, and (b) responding to a users direction to change application state differently.
Keep in mind, that a GoAngular model (returned by $sync()) is an object, which in the case of a collection of todos might look something like this:
{
"id-146ce1c6c9e-000-0": { "description": "Destroy the Death Start" },
"id-146ce1c6c9e-000-0": { "description": "Defeat the Emperor" }
}
It will of course, have a number of methods too, those can be easily stripped using the $omit method.
If we wanted to retrieve a single item from a collection that had already been synced, we might do it like this (plunkr):
$scope.todos.$sync();
$scope.todos.$on('ready', function() {
var firstKey = (function (obj) {
for (var firstKey in obj) return firstKey;
})($scope.todos.$omit());
$scope.firstTodo = $scope.todos[firstKey].description;
});
In this example, we synchronize the collection, and once it's ready retrieve the key for the first item in the collection, and assign a reference to that item to $scope.firstTodo.
If we are responding to a users input, we'll need the ID to be passed from the view based on a user's interaction, back to the controller. First we'll update our view:
<li ng-repeat="(id, todo) in todos">
{{ todo.description }}
</li>
Now we know which todo the user want's us to modify, we describe that behavior in our controller:
$scope.todos.$sync();
$scope.whichTask = function(todoId) {
console.log('this one:', $scope.todos[todoId]);
// Remove for fun
$scope.todos.$key(todoId).$remove();
}
Here's a working example: plunkr. Hope this helps :)

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)

How to know if element is already in the DOM when creating new Backbone view

here's the situation:
When page is opened for the first time, it already has prepared DOM by server(php).
If user has javascript turned on, then i want to convert my page to web app(or whatever you call it).
As soon as Javascript is initialized, Backbone fetches collection from server.
The problem is, that some of these fetched items are already on page.
Now how can i mark those items which already are in the DOM?
And how can i tie them up with the Backbone view?
Hooking up a Backbone.View to an existing DOM element is simple:
//get a referent to the view element
var $el = $("#foo");
//initialize new view
var view = new FooView({el:$el});
The view now handles the #foo element's events, and all the other View goodness. You shouldn't call view.render. If you do, it will re-render the view to the element. This means that you can't define any necessary code in the render method.
As to how to find out which elements are already in the DOM, and how to find the corresponding element for each view - that's a bit more complicated to answer without knowing exactly how your data and html looks like. As a general advice, consider using data-* attributes to match up the elements.
Let's say you have a DOM tree:
<ul id="list">
<li data-id="1">...</li>
<li data-id="2">...</li>
<li data-id="5">...</li>
</ul>
You could bind/render a model to the container like so:
var view;
//container element
var $list = $("ul#list");
//find item node by "data-id" attribute
var $item = $list.find("li[data-id='" + model.id+ "']");
if($item.length) {
//element was in the DOM, so bind to it
view = new View( {el:$item, model:model} );
} else {
//not in DOM, so create it
view = new View( {model:model} ).render();
$list.append(view.el);
}
Ok, i managed to do that like so:
var Collection = Backbone.Collection.extend({...});
var ItemView = Backbone.View.extend({...});
var ItemsView = Backbone.View.extend({
initialize: function () {
var that = this,
coll = new Collection;
coll.fetch({ success: function () {
that.collection = coll;
that.render();
}});
},
render: function () {
this.collection.each(this.addOne, this);
},
addOne: function (model) {
var selector = '#i'+model.get("id");
if( $(selector).length ) {
//If we are here, then element is already in the DOM
var itemView = new ItemView({ 'model': model, 'el': selector, 'existsInDom': true });
} else {
var itemView = new ItemView({ 'model':model });
}
}
});

Resources