Backbone: get data of item that was clicked - javascript

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.

Related

Knockout JS bind view model to multiple distributed element IDs

This is similar to my question, but it seems the solution is to create a common parent that is close by. In terms of commonality, the only way I could do that is to bind to document or something like that, but then it defeats the purpose:
Can I applyBindings to more than one DOM element using Knockout?
Is it recommended to bind a single view model instance to multiple IDs like this. I tried it, and it works in simple cases:
ko.applyBindings(newVm, document.getElementById('grapes'));
ko.applyBindings(newVm, document.getElementById('apples'));
My reasons for doing so is that I would like to use the built in functionality to bind to specific elements on a single page application, but those elements don't have a common parent.
When a binding is applied, are any copies of the view model instance created that would cause this to be a memory hog?
This is not about multiple view models to a single page view, and this is not about multiple view models to the same element. An example use case would be a serverConnection view model, where the connect and disconnect buttons are at the top in a toolbar, while the connection status is at the bottom in a status bar.
Is it recommended to bind a single view model instance to multiple IDs
No, it's not recommended. But also not necessarily wrong...
The recommended way is to use the with binding. For example:
JS
const serverConnection = new ServerConnection();
const app = new App();
ko.applyBindings({ app, serverConnection });
HTML
<body>
<header data-bind="with: serverConnection">
<button data-bind="click: connect">Connect</button>
<button data-bind="click: disconnect">Disconnect</button>
</header>
<article data-bind="with: app">
...
</article>
<footer data-bind="with: serverConnection">
<div data-bind="text: statusCode"></div>
</footer>
</body>
Runnable snippet
const serverConnection = new ServerConnection();
const app = new App(serverConnection);
ko.applyBindings({ app, serverConnection });
function App(connection) {
this.user = connection.user;
this.heading = ko.pureComputed(
() => this.user() ? `Welcome, ${this.user()}` : `Connect to get started...`
);
}
function ServerConnection() {
this.connected = ko.observable(false);
this.connecting = ko.observable(false);
this.user = ko.observable(null);
this.connect = () => {
this.connecting(true);
setTimeout(
() => {
this.connected(true);
this.user("Jane Doe");
this.connecting(false);
},
1500
)
};
this.disconnect = () => {
this.user(null);
this.connected(false);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<header data-bind="with: serverConnection">
<button data-bind="click: connect, disable: connecting">Connect</button>
<button data-bind="click: disconnect, disable: connecting">Disconnect</button>
</header>
<article data-bind="with: app">
<h2 data-bind="text: heading"></h2>
</article>
<footer data-bind="with: serverConnection">
<div data-bind="text: connected() ? '✅' : '🛑'"></div>
</footer>

Child view model modying a different child viewmodel

I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?
$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable

Async loading a template in a Knockout component

I'm pretty experienced with Knockout but this is my first time using components so I'm really hoping I'm missing something obvious! I'll try and simplify my use case a little to explain my issue.
I have a HTML and JS file called Index. Index.html has the data-bind for the component and Index.js has the ko.components.register call.
Index.html
<div data-bind="component: { name: CurrentComponent }"></div>
Index.js
var vm = require("SectionViewModel");
var CurrentComponent = ko.observable("section");
ko.components.register("section", {
viewModel: vm.SectionViewModel,
template: "<h3>Loading...</h3>"
});
ko.applyBindings();
I then have another HTML and JS file - Section.html and SectionViewModel.js. As you can see above, SectionViewModel is what I specify as the view model for the component.
Section.html
<div>
<span data-bind="text: Section().Name"></span>
</div>
SectionViewModel.js
var SectionViewModel = (function() {
function SectionViewModel() {
this.Section = ko.observable();
$.get("http://apiurl").done(function (data) {
this.Section(new SectionModel(data.Model)); // my data used by the view model
ko.components.get("dashboard", function() {
component.template[0] = data.View; // my html from the api
});
});
}
return SectionViewModel;
});
exports.SectionViewModel = SectionViewModel;
As part of the constructor in SectionViewModel, I make a call to my API to get all the data needed to populate my view model. This API call also returns the HTML I need to use in my template (which is basically being read from Section.html).
Obviously this constructor isn't called until I've called applyBindings, so when I get into the success handler for my API call, the template on my component is already set to my default text.
What I need to know is, is it possible for me to update this template? I've tried the following in my success handler as shown above:
ko.components.get("section", function(component) {
component.template[0] = dataFromApi.Html;
});
This does indeed replace my default text with the html returned from my API (as seen in debug tools), but this update isn't reflected in the browser.
So, basically after all that, all I'm really asking is, is there a way to update the content of your components template after binding?
I know an option to solve the above you might think of is to require the template, but I've really simplified the above and in it's full implementation, I'm not able to do this, hence why the HTML is returned by the API.
Any help greatly appreciated! I do have a working solution currently, but I really don't like the way I've had to structure the JS code to get it working so a solution to the above would be the ideal.
Thanks.
You can use a template binding inside your componente.
The normal use of the template bindign is like this:
<div data-bind="template: { name: tmplName, data: tmplData }"></div>
You can make both tmplData and tmplName observables, so you can update the bound data, and change the template. The tmplName is the id of an element whose content will be used as template. If you use this syntax you need an element with the required id, so, in your succes handler you can use something like jQuery to create a new element with the appropriate id, and then update the tmplname, so that the template content gets updated.
*THIS WILL NOT WORK:
Another option is to use the template binding in a different way:
<div data-bind="template: { nodes: tmplNodes, data: tmplData }"></div>
In this case you can supply directly the nodes to the template. I.e. make a tmplNodes observable, which is initialized with your <h3>Loading...</h3> element. And then change it to hold the nodes received from the server.
because nodesdoesn't support observables:
nodes — directly pass an array of DOM nodes to use as a template. This should be a non-observable array and note that the elements will be removed from their current parent if they have one. This option is ignored if you have also passed a nonempty value for name.
So you need to use the first option: create a new element, add it to the document DOM with a known id, and use that id as the template name. DEMO:
// Simulate service that return HTML
var dynTemplNumber = 0;
var getHtml = function() {
var deferred = $.Deferred();
var html =
'<div class="c"> \
<h3>Dynamic template ' + dynTemplNumber++ + '</h3> \
Name: <span data-bind="text: name"/> \
</div>';
setTimeout(deferred.resolve, 2000, html);
return deferred.promise();
};
var Vm = function() {
self = this;
self.tmplIdx = 0;
self.tmplName = ko.observable('tmplA');
self.tmplData = ko.observable({ name: 'Helmut', surname: 'Kaufmann'});
self.tmplNames = ko.observableArray(['tmplA','tmplB']);
self.loading = ko.observable(false);
self.createNewTemplate = function() {
// simulate AJAX call to service
self.loading(true);
getHtml().then(function(html) {
var tmplName = 'tmpl' + tmplIdx++;
var $new = $('<div>');
$new.attr('id',tmplName);
$new.html(html);
$('#tmplContainer').append($new);
self.tmplNames.push(tmplName);
self.loading(false);
self.tmplName(tmplName);
});
};
return self;
};
ko.applyBindings(Vm(), byName);
div.container { border: solid 1px black; margin: 20px 0;}
div {padding: 5px; }
.a { background-color: #FEE;}
.b { background-color: #EFE;}
.c { background-color: #EEF;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="byName" class="container">
Select template by name:
<select data-bind="{options: tmplNames, value: tmplName}"></select>
<input type="button" value="Add template"
data-bind="click: createNewTemplate"/>
<span data-bind="visible: loading">Loading new template...</span>
<div data-bind="template: {name: tmplName, data: tmplData}"></div>
</div>
<div id="tmplContainer" style="display:none">
<div id="tmplA">
<div class="a">
<h3>Template A</h3>
<span data-bind="text: name"></span> <span data-bind="text: surname"></span>
</div>
</div>
<div id="tmplB">
<div class="b">
<h3>Template B</h3>
Name: <span data-bind="text: name"/>
</div>
</div>
</div>
component.template[0] = $(data)[0]
I know this is old, but I found it trying to do the same, and the approcah helped me come up with this in my case, the template seems to be an element, not just raw html

Not sure how to template object of arrays using Backbone and Underscore templates

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/

How to render and save dynamic properties of a Backbone Model?

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.

Categories