I am totally lost on how to implement a solution for the following problem:
I have a Backbone View that is populated with the properties from a Backbone Model. In this view, the attributes are displayed and for one of the attributes, the user needs to be able to add or modify the existing ones. This would be best suited to be an array of properties within the Backbone Model (I think).
I have added a test case in the js fiddle here
As you can see in the fiddle, the dog's favorite_snacks can be added to by the user, but I have been completely struggling on how to write the code to parse and render this in upon instantiating the view and then save them back to the mysql database successfully.
I would greatly appreciate any help on this, I've been banging my head against my desk all weekend trying to figure it out.
It is always a better idea make sure that views and models behave independently and completely decoupled so that your code can be managed at a later time.
So in this case each Pet can have one or many favorite snacks. So basically you are expecting a collection of snacks here and not a single snack model.
So it is better to create a separate Snacks Collection and then a Snack Model. Then maintain a separate view for SnackListItem and then iterate over the List of SnackCollection and render the view for each item..
So create separate templates for both of them
<div id="foo"></div>
<script type="text/template" id="pet-view-template">
<p> <span><b> Dog Name: </b> </span> <%= name %> </p>
<p> <span><b> Dog Color: </b></span> <%= color %> </p>
<h4> favorite snacks </h4>
<ul class="snacks-list">
</ul>
snack name: <input type="text" class="snack-name" />
cost : <input type="text" class="snack-cost" />
<button class="add-snack">add snack</button >
</script>
<script type="text/template" id="snack-view-template">
<b>snack name:</b> <span> <%=favorite_snacks %> </span> ::
<b>cost: </b><span> <%= favorite_snack_cost %> </span>
<b class="toggle-change"> Change </b>
<span class="modify-fields hide">
<b class="modify">snack name:</b> <input type="text" class="modify-name" data-key="favorite_snacks" />
<b class="modify">snack cost:</b> <input type="text" class="modify-cost" data-key="favorite_snack_cost" />
</span>
</script>
I have created a separate view and models for the Snacks collection. This can still be optimized.
Javascript
// Create a Dog Model
var Dog = Backbone.Model.extend({
defaults: {
name: 'New dog',
color: 'color'
}
});
// Create a collection of dogs
var Dogs = Backbone.Collection.extend({
model: Dog
});
// Create a model for Snacks
var Snack = Backbone.Model.extend({
defaults: {
favorite_snacks: 'bacon',
favorite_snack_cost: '52'
}
});
// Create a collection of Snacks
var Snacks = Backbone.Collection.extend({
model: Snack
});
// Define the model for a Dog
var dog = new Dog({
name: "Spot",
color: "white"
});
// Create a View for the Snack Item
var SnackItemView = Backbone.View.extend({
tagName : 'li',
className: 'snacks',
template: _.template($('#snack-view-template').html()),
initialize: function() {
// Need to bind to save the context of this to the view
_.bind('toggleChange', this);
// Need to listen to the Model change event and render the view again
// as the new data has to be reflected
this.listenTo(this.model, 'change' , this.render);
},
// Assign events for the fields inside Snacks view
events : {
'click .toggle-change' : 'toggleChange',
'change input' : 'modifyData'
},
toggleChange: function() {
var $change = $('.modify-fields', this.$el);
$change.hasClass('hide') ? $change.removeClass('hide') : $change.addClass('hide');
},
// This will captue the data from the inputs and trigger the change event on the model
modifyData: function(e) {
var value = $(e.currentTarget).val(),
key = $(e.currentTarget).data('key');
this.model.set(key,value);
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var PetView = Backbone.View.extend({
el: '#foo',
template: _.template($('#pet-view-template').html()),
initialize: function () {
this.collection = (this.collection && this.collection instanceof Backbone.Collection) || new Snacks(Snack);
},
events: {
'click .add-snack' : 'addSnack'
},
addSnack: function () {
// Render new snack Item
var newSnack = new Snack({
favorite_snacks: $('.snack-name', this.$el).val(),
favorite_snack_cost: $('.snack-cost', this.$el).val()
});
this.renderSnackView(newSnack);
// Clear the inputs
$('input', this.$el).val('');
},
// render each snack view
renderSnackView: function (snack) {
var snackView = new SnackItemView({
model: snack
});
$('.snacks-list', this.$el).append(snackView.el);
snackView.render();
},
render: function () {
var thisView = this;
// Append the PetView
this.$el.html(this.template(this.model.toJSON()));
// Iterate over each snack collection of the pet and render that item
_.each(this.collection.models, function (snack) {
thisView.renderSnackView(snack);
});
return this;
}
});
var petView = new PetView({
model: dog
});
petView.render();
Check the working fiddle
I have used comments to explain some of the code. It is tough to learn backbone initially as i just started working with it recently and i know the trouble I went thru . but once you get used to it it is very simple yet powerful.. Hope this helps :)
But when you try to build an application , you need to destroy the views and events that are not being used in order to reduce memory leaks.
Related
I have single view displaying investments + two others which are modals to register new investment which show up when user clicks 'add' (two modals because of two steps of registration). I created factory which is used in step1 and then in step2 in order to keep information regarding investment being registered - it works when you switch between step1 and step2, back and forth.
The problem is that within first view displaying investments I have icon "edit" and within its handler (edit method) I assign selected investment to factory but no change is reflected in step1 view, alas.
View displaying investments:
var module = angular.module("application", []);
module.controller("investmentsController", function ($scope, investmentsFactory, newInvestmentFactory) {
$scope.edit = function (id) {
for (var i = 0; i < $scope.Investments.length; i++) {
if ($scope.Investments[i].Id == id) {
newInvestmentFactory.update($scope.Investments[i]);
}
}
$("#addInvestmentStep1Modal").modal("show");
};
});
View step1 of registration
var module = angular.module("application");
module.factory("newInvestmentFactory", function () {
return {
investment: {
Name: "",
Description: "",
Category: "",
InterestRate: "",
Duration: "",
AmountRange: "",
Url: "",
Subscription: 0,
PaymentType: "",
Icon: ""
},
update: function (investment) {
this.investment = investment;
}
};
});
module.controller("newInvestmentStep1Controller", function ($scope, newInvestmentFactory) {
$scope.Investment = newInvestmentFactory.investment;
});
View step2 of registration
var module = angular.module("application");
module.controller("newInvestmentStep2Controller", function ($scope, newInvestmentFactory) {
$scope.Investment = newInvestmentFactory.investment;
});
The step1 view displaying registration is following
<form id="newInvestmentStep1Form" class="form-horizontal">
<div class="input-group">
<span class="input-group-addon input-group-addon-register">Name</span>
<input id="Name" name="Name" type="text" class="form-control" ng-model="Investment.Name" required title="Pole wymagane" />
</div>
Assignining new object to factory's object (newInvestmentFactory.investment) does not seem to be working but when I assign brand new value to some property of factory like
newInvestmentFactory.investment.Name = "name"
then it displays value correctly.
I can only suspect newInvestmentFactory's update method code. It is reassigning investment object to new investment object like this.investment = investment. By that line new investment object gets created, and old investment loose the reference. To keep the investment object to not create a new variable in update method, you could use angular.extend/angular.merge method. This method will not create a new reference of an object, but it ensures that all object property got updated.
update: function (investment) {
angular.extend(this.investment, investment);
}
In your step controllers
$scope.Investment = newInvestmentFactory.investment;
is just one time assignment to $scope variable, this is not two way binding, so even if value of newInvestmentFactory.investment changes scope won't be updated. What you can do is to watch the factory variable newInvestmentFactory.investment and on change update the scope manually.
Hope this helps
I have a collection where the data is returned looking like:
{
"departments": ["Customer Support", "Marketing"],
"classes": ["Planning", "Drawing"]
}
I'm not overly sure how to use underscore template loops to output each of the departments, right now I'm using ._each but my output is object Object. Can anyone advise how to resolve this?
Fiddle: http://jsfiddle.net/kyllle/aatc70Lo/7/
Template
<script type="text/template" class="js-department">
<select>
<% _.each(departments, function(department) { %>
<option value="<% department %>"><% department %></option>
<% }) %>
</select>
</script>
JS
var Department = Backbone.Model.extend();
var Departments = Backbone.Collection.extend({
model: Department,
parse: function(response) {
return response;
}
});
var DepartmentView = Backbone.View.extend({
template: '.js-department',
initialize: function() {
console.log('DepartmentView::initialize', this.collection.toJSON());
},
render: function() {
this.$el.html( this.template( this.collection.toJSON() ) );
}
});
var departments = new Departments({
"departments": ["Customer Support", "Marketing"]
}, {parse:true});
var departmentView = new DepartmentView({
collection: departments
});
document.body.innerHTML = departmentView;
You are not even calling render(), so your template is never even executed, and the object Object output has nothing to do to your template.
After you run render() you will realize
template: '.js-department'
doesn't work, because it is not Marionette, and Backbone will not compile the html by a selector for you. So you will replace it with something like this:
template: _.template($('.js-department').html())
Then you will have to realize this.collection is an array, that only has one item, so if you just want to render that first item, you will send to it to template:
this.$el.html( this.template( this.collection.first().toJSON() ) );
Then you will have to realize departmentView is a Backbone.View instance object, and isn't html itself. It has the el property which is the DOM element of this view instance, and $el property, which is the same DOM element wrapped with jQuery.
document.body.innerHTML = departmentView.el still will not work, because innerHTML expects a string. So you could instead do something like
document.body.appendChild( departmentView.el ); or
departmentView.$el.appendTo( document.body ); with jquery.
(For the last one to work render must return this)
Working jsfiddle: http://jsfiddle.net/yuraji/zuv01arh/
I want to try out Backbone.js and started with the famous TodoMVC-App. I want to add some more attributes via input fields (orgiginally there is only one input field with "todo"), but I cant figure out how.
Before I was trying Angular.js and that was a little bit easier - now I am stucked at the point how I should add more attributes per input fields.
Can anyone give me a hint whats the best/easiest way to achieve this?
Some relevant code snippets:
Index.html:
<tr class="userInputs" >
<td><input id="toggle-all" type="checkbox"></td>
<td><input type="text" id="new-todo" placeholder="What needs to be done?" autofocus style="width: 150px"/></td>
<td><input type="text" id="newQuantity"/></td>
<td colspan="2"><a ><img src="img/plus.png" id="addItem"></a></td>
</tr>
model/todo.js
app.Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
quantity: 0,
completed: false
}
});
views/app.js:
initialize: function() {
this.allCheckbox = this.$('#toggle-all')[0];
this.$input = this.$('#new-todo');
this.$footer = this.$('#footer');
this.$main = this.$('#main');
this.listenTo(app.Todos, 'add', this.addOne);
this.listenTo(app.Todos, 'reset', this.addAll);
this.listenTo(app.Todos, 'change:completed', this.filterOne);
this.listenTo(app.Todos, 'filter', this.filterAll);
this.listenTo(app.Todos, 'all', this.render);
app.Todos.fetch();
}
addOne: function( todo ) {
var view = new app.TodoView({ model: todo });
$('#todo-list').append( view.render().el );
}
newAttributes: function() {
return {
title: this.$input.val().trim(),
quantity: this.$input.val().trim(),
order: app.Todos.nextOrder(),
completed: false
};
}
createOnEnter: function(e) {
app.Todos.create( this.newAttributes() );
this.$input.val('');
}
Hope this is enough information, otherwise please tell me!
Follow the way it's done for the title.
Add a new input.
Change the appView#newAttributes method to pass the new input's value to the model.
Change the appView#createOnEnter method to reset the field.
Change the #item-template to include the new attribute in your Todo template.
Everything else will be automatic (including setting the new attribute to the model (passed as argument) and passing the new attribute to the template (because we use the Model#toJSON method)).
Edit:
this.$input is a reference to this.$('#new-todo') (see the initialize method) so its val is the title. You need to create a new var:
In initialize:
this.$quantity = this.$('#newQuantity');
In newAttributes:
quantity: this.$quantity.val();
// you're not making any check here
// add one if necessary (you can use underscore), eg
// _.isNumber(quantity = this.$('#newQuantity')) ? quantity : 0
// number being declared before, else it'd be global
In createOnEnter:
this.$quantity.val('');
I have a simple page where I display a list of books and a few details about the book, title, price, description. Data is pulled from a JSON file.
When I click on any of the books listed, a lightbox (bootstrap modal) fires up where I'd like to show the title of the book that was clicked.
User will be able to write a comment so I'd also like to get then send the book ID.
Not sure what is the best way to get the data from the book that was clicked?
Here is my code so far (including lightbox):
Backbone:
var Book = Backbone.Model.extend();
var BookList = Backbone.Collection.extend({
model: Book,
url: 'json/books.json'
});
var BookView = Backbone.View.extend({
el: '.booksList',
template: _.template($('#booksTemplate').html()),
render: function(){
_.each(this.model.models, function(model){
this.$el.append(this.template({
data: model.toJSON()
}));
}, this);
return this;
}
});
var AppView = Backbone.View.extend({
el: 'body',
initialize: function(){
var bookList = new BookList();
var bookView = new BookView({
model: bookList
});
bookList.bind('reset', function(){
bookView.render();
});
bookList.fetch();
}
});
var appView = new AppView();
Template:
<script id="booksTemplate" type="text/template">
<div class="book">
<div class="bookDetails">
<h3><%= data.title %></h3>
<p><%= data.price %></p>
</div>
<p><%= data.description %></p>
bid
</div>
<div id="myModal" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3><%= data.title %></h3>
</div>
<div class="modal-body">
<form action="#" method="post">
<input type="text" name="comment" id="comment" />
</form>
</div>
<div class="modal-footer">
Close
</div>
</div>
</script>
Listen to the events in your view. Source.
So basically you'll have something like this in your view:
var BookView = Backbone.View.extend({
el: '.booksList',
template: _.template($('#booksTemplate').html()),
render: function(){
_.each(this.model.models, function(model){
this.$el.append(this.template({
data: model.toJSON()
}));
}, this);
return this;
},
events: {
'click': 'openModal'
// you could also use 'click selector', see the doc
},
openModal: function() {
// here the context is your view
// so this.model will give you your collection, hence access to your data
}
});
However, I personally think that you should have several views, each for one model (=book), instead of a whole view for the collection. But hey, that's just an opinion.
Edit: details
I personally never create views for a collection. I prefer wrapping collections in another model (eg, as you have a list of books, a bookshelf...). But that's just if you need a unique element on top of the list of views.
To illustrate, say you ordered your books by genre. You'd want a wrapping view to display a title (to tell the user the genre). So you could use a wrapping model for your collection.
Now you simply want to display all of your books as one. You could only add as many views as you have books, inside some div or ul element. Hence you'd not need to wrap your collection.
I could go on forever about where, when and how I'm creating my views but that's not the point, nor am I qualified to do so (haven't had any computer science education, so you may question everything I'm saying, I won't resent you). So basically, you could just change your code to:
initialize: function(){
var bookList = new BookList; // I'm removing the parenthesis here
// I simply like to separate "new Booklist" which makes a new object
// from "Booklist()" which just calls the function
bookList.each(function(book) {
new BookView({model: book});
// here you may not need "book" and use "this" instead, not sure though
});
Then there's the question of the binding. Again, I'll let you search for your solution, but it could be as easy as doing the binding inside the views' initialize function. There are a lot of possibilities.
I have a ASP.NET MVC 4 app with model, that contains and colection (IEnumerable<T> or IList<T>), i.e.:
class MyModel
{
public int Foo { get; set; }
public IList<Item> Bar { get; set; }
}
class Item
{
public string Baz { get; set; }
}
And I render the data in view with classic #for..., #Html.EditorFor... ad so on. Now there's a need to add on client side to add dynamically new items and then post it back to server.
I'm looking for an easy solution to handle the adding (in JavaScript), aka not manually creating all the inputs etc. Probably to get it somehow from editor template view. And to add it the way that when the form is submitted back to server the model binder will be able to properly create the IList<T> collection, aka some smart handling of inputs' names. I read a bunch of articles, but nothing that was easy and worked reliably (without magic strings like collection variable names, AJAX callbacks to server, ...).
So far this looks promising, but I'd like to rather rely on rendering (items known in advance) on server.
I'm not sure what do you mean 'collection variable names' and probably my solution is kind of magic you noticed.
My solution is based on copying existing editor for element and altering input names via Javascript.
First of all, we need to mark up our editor. This is a code of form outputs editor for collection
#for (var i = 0; i < Model.Count; i++)
{
<div class="contact-card">
#Html.LabelFor(c => Model[i].FirstName, "First Name")
#Html.TextBoxFor(c => Model[i].FirstName)
<br />
#Html.LabelFor(c => Model[i].LastName, "Last Name")
#Html.TextBoxFor(c => Model[i].LastName)
<br />
#Html.LabelFor(c => Model[i].Email, "Email")
#Html.TextBoxFor(c => Model[i].Email)
<br />
#Html.LabelFor(c => Model[i].Phone, "Phone")
#Html.TextBoxFor(c => Model[i].Phone)
<hr />
</div>
}
Our editor is placed into div with class contact-card. On rendering, ASP.NET MVC gives names like [0].FirstName, [0].LastName ... [22].FirstName, [22].LastName to inputs used as property editors. On submitting Model Binder converts this to collection of entities based both on indexes and property names.
Next we create javascript function that copies last editor and increases index in brackets by 1. On submitting it adds additional element to collection:
var lastContent = $("#contact-form .contact-card").last().clone();
$("#contact-form .contact-card").last().after(lastContent);
$("#contact-form .contact-card")
.last()
.find("input")
.each(function () {
var currentName = $(this).attr("name");
var regex = /\[([0-9])\]/;
var newName = currentName.replace(regex, '[' + (parseInt(currentName.match(regex)[1]) + 1) + ']');
$(this).val('');
$(this).attr('name', newName);
});
VOILA!! On submitting we will get one more element!
At the end I did similar stuff what STO was suggesting, but with the custom (non-linear) indices for collections suggested by Phil Haack.
This uses manual naming of elements (so I'm not binding directly to the model) and I can use custom instances (for empty element templates). I've also created some helper methods to generate me the code for the instance, so it's easier to generate code for actual instances from the model or empty ones.
I did this with help of Backbone (for file uploader) where i insert template whenever user click #addButton
View:
#using Telerik.Web.Mvc.UI
#{
ViewBag.Title = "FileUpload";
Layout = "~/Areas/Administration/Views/Shared/_AdminLayout.cshtml";
}
<div id="fileViewContainer" class="span12">
<h2>File upload</h2>
#foreach(var fol in (List<string>)ViewBag.Folders){
<span style="cursor: pointer;" class="uploadPath">#fol</span><br/>
}
#using (Html.BeginForm("FileUpload", "CentralAdmin", new { id = "FileUpload" }, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<label for="file1">Path:</label>
<input type="text" style="width:400px;" name="destinacionPath" id="destinacionPath"/><br />
<div id="fileUploadContainer">
<input type="button" class="addButton" id="addUpload" value="Add file"/>
<input type="button" class="removeButton" id="removeUpload" value="Remove file"/>
</div>
<input type="submit" value="Upload" />
}
</div>
<script type="text/template" id="uploadTMP">
<p class="uploadp"><label for="file1">Filename:</label>
<input type="file" name="files" id="files"/></p>
</script>
#{
Html.Telerik().ScriptRegistrar().Scripts(c => c.Add("FileUploadInit.js"));
}
FileUploadInit.js
$(document).ready(function () {
var appInit = new AppInit;
Backbone.history.start();
});
window.FileUploadView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, 'render', 'addUpload', 'removeUpload', 'selectPath');
this.render();
},
render: function () {
var tmp = _.template($("#uploadTMP").html(), {});
$('#fileUploadContainer').prepend(tmp);
return this;
},
events: {
'click .addButton': 'addUpload',
'click .removeButton': 'removeUpload',
'click .uploadPath': 'selectPath'
},
addUpload: function (event) {
this.render();
},
removeUpload: function (event) {
$($('.uploadp')[0]).remove();
},
selectPath: function (event) {
$('#destinacionPath').val($(event.target).html());
}
});
var AppInit = Backbone.Router.extend({
routes: {
"": "defaultRoute"
},
defaultRoute: function (actions) {
var fileView = new FileUploadView({ el: $("#fileViewContainer") });
}
});
In Controller you keep your code
I Hope this will help.