My backbone model has a boolean value (isRegistered). When I render out the view I want to have a checkbox checked or unchecked depending on the true/false value of the boolean.
My current effort looks like this:
<input id="isRegisteredCheckbox" checked="<%= isRegistered ? 'checked': ''"/>
this doesn't work because according to the W3C Specification the checked attribute needs to be completely removed to uncheck a checkbox.
How do I do it using backbone template?
You could use a test to enclose checked='checked'
<input id="isRegisteredCheckbox" <% if (isRegistered) { %>checked="checked"<% } %> />
You don't need the checked= part. just print out checked in the tag if it needs to be checked.
EDIT
Now that we've determined that just printing "checked" out is valid html, you might try for simplicity:
render:
var registered;
var tmpl = _.template(your template);
isRegistered ? registered = 'checked' : registered = '';
var tmpl_data = _.extend(this.model.toJSON(), {registered: registered}); // or whatever values you need to add
$(this.el).html(tmpl(tmpl_data));
template:
<input type="checkbox" {{ registered }}>
No need for messy conditionals in your template using this method.
I used to use a Decorator for this cases. I expose here an example of how it can looks like:
// code simplified and not tested
var MyModelDecorator = Backbone.Model.extend({
initialize: function( opts ){
this.model = opts.model;
},
toJSON: function(){
var json =
_.extend(
this.model.toJSON(),
{
checked: this.checked(),
css_classes: this.cssClasses()
}
);
return json;
},
checked: function(){
result = "";
if( this.model.get( "checked" ) ) result += "checked=\"true\"";
return result;
},
cssClasses: function(){
result = "";
if( this.model.get( "checked" ) ) result += " checked";
if( this.model.get( "key" ) == "value" ) result += " important";
return result;
}
});
I have added an extra css_classes decorator attribute so you can see this approach can be a common solution for several situations.
Your View.render can look like this:
// code simplified and not tested
var myView = Backbone.View.extend({
template: _.template( "<input id=\"isRegisteredCheckbox\" <%= checked %> class=\"<%= css_classes %>\" />" ),
render: function(){
var decorator = new MyModelDecorator({ model: this.model });
this.$el.html( this.template( decorator.toJSON() ) );
}
});
Here is a very simple way of doing it.
<input <% if(isRegistered) print("CHECKED"); %> type="checkbox" id="isRegisteredCheckbox" />
Notice that i use <% and not <%= for the condition.
You could just set the value of isRegistered to "CHECKED" or "" in your model and call
<input id="isRegisteredCheckbox" <%= registered %> />
with something like
serialize: function() {
var isChecked = (isRegistered) ? "CHECKED" : "";
return {registered : ischecked};
},
I had a similar issue where I inherited someone's Django + hamlpy (HAML) + Backbone.js + Undescore.js with Mustache templates (what a mess!)
Haml processing in hamlpy didn't exactly like:
%input{ :checked => {{isChecked}} ? true : None }
Or any similar thing. I got a Django hamlpy stacktrace.
I managed to hack it by using Mustache's inverted sections (see: http://mustache.github.com/mustache.5.html)
{#isChecked}
%input{:checked => 'true' }
{/isChecked}
{^isChecked}
%input{ ... without the checked attribute ... }
{/isChecked}
Anyway, hope that helps some poor Googler from wasting hours of their life force!
Related
I'm using knockout's foreach loop that fetches the values from an array and displays it in a href tag.
This all works well but once I use javascript's onclick (I need this onclick as I am using InAppBrowser plugin for mobiles) and uses the variables inside it, it doesn't work. See example here:
<div data-bind="foreach: consumerData" style="margin-bottom:100px;">
<table>
<tr>
<td colspan="2">
<p style="font-size:larger; margin-bottom:5px;">
<a style="text-decoration:none;"
data-bind="attr: { href: 'http://domain:8080/dsservlet/'+$data[0]+'.png?key=DK188961' },
text: $data[1]" target="_blank"
onclick="window.open('http://domain:8080/dsservlet/'+$data[0]+'.png?key=DK188961',
'_blank', 'location=yes'); return false;"></a></p>
</td></tr>
</table>
</div>
As you can see the $data[0] works fine inside the data-bind attribute. But using the same $data[0] inside the onclick doesn't work which is still inside the foreach loop. I assume I need to declare a javascript variable to be able to make it work, but how do I declare it inside the foreach loop? I need to declare it inside the foreach loop as the array varies with different values.
See the javscript part here:
var ViewModel = function() {
this.consumerData = ko.observableArray([[174302,"BUSINESS - APPLICATION TO CONDUCT A BUSINESS FROM HOME.pdf",".pdf","DK89639"],[120183,"Glovent-Brochure.pdf",".pdf","DK472894"]]);
}
ko.applyBindings(new ViewModel());
With Knockout there's a different way to handle onclick: use a click binding handler. Like this:
var ViewModel = function() {
var self = this;
this.consumerData = ko.observableArray([[174302,"BUSINESS - APPLICATION TO CONDUCT A BUSINESS FROM HOME.pdf",".pdf","DK89639"],[120183,"Glovent-Brochure.pdf",".pdf","DK472894"]]);
this.openServlet = function(data) {
window.open('http://domain:8080/dsservlet/'+data[0]+'.png?key=DK188961', '_blank', 'location=yes');
};
};
ko.applyBindings(new ViewModel());
<a data-bind="attr: { href: 'http://domain:8080/dsservlet/'+$data[0]+'.png?key=DK188961' },
click: $parent.openServlet
text: $data[1]"
target="_blank"></a>
Please read linked documentation carefully, it'll have the answers to many follow-up questions that might arise.
Finally, consider converting consumerData to a proper sub view model in its own right, instead of a raw array of data. This would allow you to create the href in an observable or computed observable, thus also allowing you to unit test it.
As a footnote, if you really need to have an onclick you could set it using the attr binding you also used for href. So for example:
var ConsumerData = function(data) {
var self = this;
self.id = data[0];
self.filename = data[1];
self.extension = data[2];
self.code = data[3];
self.url = 'http://domain:8080/dsservlet/' + self.id + '.png?key=DK188961';
self.openServlet = function() {
window.open(self.url, '_blank', 'location=yes');
};
self.onclickValue = "window.open('http://domain:8080/dsservlet/'+data[0]+'.png?key=DK188961', '_blank', 'location=yes'); return false";
// Overwrite them again for testing on StackOverflow (window.open is crap for testing)
self.openServlet = function() { alert(self.url); };
self.onclickValue = "alert('" + self.url + "'); return false;";
};
var ViewModel = function() {
this.consumers = ko.observableArray([
new ConsumerData([174302, "BUSINESS - APPLICATION TO CONDUCT A BUSINESS FROM HOME.pdf", ".pdf", "DK89639"]),
new ConsumerData([120183, "Glovent-Brochure.pdf", ".pdf", "DK472894"])
]);
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div data-bind="foreach: consumers">
<p>
<a data-bind="attr: { href: url, onclick: onclickValue }, click: openServlet, text: filename" target="_blank"></a>
</p>
</div>
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>
In working with the API from themoviedb.com, I'm having the user type into an input field, sending the API request on every keyup. In testing this, sometimes the movie poster would be "null" instead of the intended poster_path. I prefer to default to a placeholder image to indicate that a poster was not found with the API request.
So because the entire poster_path url is not offered by the API, and since I'm using an AngularJS ng-repeat, I have to structure the image tag like so (using dummy data to save on space):
<img ng-src="{{'http://example.com/'+movie.poster_path}}" alt="">
But then the console gives me an error due to a bad request since a full image path is not returned. I tried using the OR prompt:
{{'http://example.com/'+movie.poster_path || 'http://example.com/missing.jpg'}}
But that doesn't work in this case. So now with the javascript. I can't seem to get the image source by using getElementsByTagName or getElementByClass, and using getElementById seems to only grab the first repeat and nothing else, which I figured would be the case. But even then I can't seem to replace the image source. Here is the code structure I attempted:
<input type="text" id="search">
<section ng-controller="movieSearch">
<article ng-repeat="movie in movies">
<img id="myImage" src="{{'http://example.com/'+movie.poster_path}}" alt="">
</article>
</section>
<script>
function movieSearch($scope, $http){
var string,
replaced,
imgSrc,
ext,
missing;
$(document).on('keyup', function(){
string = document.getElementById('search').value.toLowerCase();
replaced = string.replace(/\s+/g, '+');
$http.jsonp('http://example.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
});
imgSrc = document.getElementById('myImage').src;
ext = imgSrc.split('.').pop();
missing='http://example.com/missing.jpg';
if(ext !== 'jpg'){
imgSrc = missing;
}
});
}
</script>
Any ideas with what I'm doing wrong, or if what I'm attempting can even be done at all?
The first problem I can see is that while you are setting the movies in a async callback, you are looking for the image source synchronously here:
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
});
// This code will be executed before `movies` is populated
imgSrc = document.getElementById('myImage').src;
ext = img.split('.').pop();
However, moving the code merely into the callback will not solve the issue:
// THIS WILL NOT FIX THE PROBLEM
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
// This will not solve the issue
imgSrc = document.getElementById('myImage').src;
ext = img.split('.').pop();
// ...
});
This is because the src fields will only be populated in the next digest loop.
In your case, you should prune the results as soon as you receive them from the JSONP callback:
function movieSearch($scope, $http, $timeout){
var string,
replaced,
imgSrc,
ext,
missing;
$(document).on('keyup', function(){
string = document.getElementById('search').value.toLowerCase();
replaced = string.replace(/\s+/g, '+');
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
$scope.movies.forEach(function (movie) {
var ext = movie.poster_path && movie.poster_path.split('.').pop();
// Assuming that the extension cannot be
// anything other than a jpg
if (ext !== 'jpg') {
movie.poster_path = 'missing.jpg';
}
});
});
});
}
Here, you modify only the model behind you view and do not do any post-hoc DOM analysis to figure out failures.
Sidenote: You could have used the ternary operator to solve the problem in the view, but this is not recommended:
<!-- NOT RECOMMENDED -->
{{movie.poster_path && ('http://domain.com/'+movie.poster_path) || 'http://domain.com/missing.jpg'}}
First, I defined a filter like this:
In CoffeeScript:
app.filter 'cond', () ->
(default_value, condition, value) ->
if condition then value else default_value
Or in JavaScript:
app.filter('cond', function() {
return function(default_value, condition, value) {
if (condition) {
return value;
} else {
return default_value;
}
};
});
Then, you can use it like this:
{{'http://domain.com/missing.jpg' |cond:movie.poster_path:('http://domain.com/'+movie.poster_path)}}
Does Emberjs provide selectionBinding for the checkbox to handle selected/checked checkbox options.
If yes, how to do it?
Bind to the checked property of Ember.Checkbox, see http://jsfiddle.net/5pnVg/:
Handlebars:
{{view Ember.Checkbox checkedBinding="App.objController.isChecked" }}
JavaScript:
App.objController = Ember.Object.create({
isChecked: true,
_isCheckedChanged: function(){
var isChecked = this.get('isChecked');
console.log( 'isChecked changed to %#'.fmt(isChecked) );
}.observes('isChecked')
});
Okay, so this is kinda old but I stumbled upon this too. I had my checkbox options delivered to the route's model in an array. The problem really is with achieving two way binding (if this is the goal). This is how I've done it:
App.ItemEditController = Ember.ObjectController.extend({
isRound: function () {
return ( this.get('model.shapes').find(function(item) { return (item === 'round') }) );
}.property('model.shapes'),
isOval: function () {
return ( this.get('model.shapes').find(function(item) { return (item === 'oval') }) );
}.property('model.shapes'),
isIrregular: function () {
return ( this.get('model.shapes').find(function(item) { return (item === 'irregular') }) );
}.property('model.shapes'),
shapes: function () {
var self = this;
['round','oval','irregular'].map(function(item) {
var shapes = self.get('model.shapes');
shapes = shapes.toArray();
if (self.get('is' + item.capitalize())) {
if (shapes.indexOf(item) < 0)
shapes.push(item);
} else {
if (shapes.indexOf(item) >= 0)
shapes.splice(shapes.indexOf(item),1);
}
self.set('model.shapes', shapes);
});
}.observes('isRound', 'isOval', 'isIrregular')
});
So here I've setup the properties to set themselves based on their presence in the model's shapes array and set an observer that checks these properties to resets the model's shapes array if required.
Now in the Item template we can bind to the shapes as we always do (however you do):
Shapes:
{{#each shape in this.shapes}}
<span class="label label-default">{{shape}}</span><br />
{{else}}
No shape selected!
{{/each}}
and in the ItemEdit template we bind to the edit controller's properties:
Shapes:
Round: {{input type="checkbox" checked=isRound}}
Oval: {{input type="checkbox" checked=isOval}}
Irregular: {{input type="checkbox" checked=isIrregular}}
Hope this helps with anyone struggling with this type of manual two way binding and you'll get all your checked options in your model in one go.
I'm new to backbone and trying to set it up in Sinatra, but I can't seem to get a simple create working.
I've set up my model/collection as so:
var TEAM_ID = window.location.pathname.split('/')[1]; // From url
$(function () {
var TeamMember = Backbone.Model.extend({
defaults: {
name : ""
}
});
var TeamMembers = Backbone.Collection.extend({
model: TeamMember,
url: "/" + TEAM_ID + "/team-members.json"
});
var teamMembers = new TeamMembers;
var TeamMemberView = Backbone.View.extend({
events: {
"click #new-team-member-form .submit-button" : "handleNewTeamMember"
},
handleNewTeamMember: function(data) {
var inputField = $('input[name=new_team_member_name]');
console.log("Pre create");
// This doesn't get sent to the server!!
var teamMember = teamMembers.create({name: inputField.val());
console.log("Post create");
return false; // Don't submit form
},
render: function() {
console.log("Render team member");
return this;
}
});
// ...
var teamMemberView = new TeamMemberView({el: $('#week-view')});
});
The html looks like:
<table id="week-view">
<!-- ... -->
<form id="new-team-member-form" action="/some-add-url" method="post">
<fieldset class="new-object-fieldset" title="New team member">
<legend>New team member</legend>
<label for="new_team_member_name">Add new</label>
<input type="text" name="new_team_member_name" title="Add member" class="new-object-text-box" />
<button type="submit" name="new_team_member" value="new_team_member" class="submit-button">+</button>
<div id="help-new"></div>
</fieldset> <!-- New team member -->
</form>
<!-- ... -->
and the ruby looks like:
post '/:team_id/team-members.json' do
logger.info("Add team member (json): #{params}")
end
However, the sinatra server only shows params[:team_id], without the name parameter on the teamMembers.create line. Am I doing something stupid in backbone? Not initialising something properly?
I've looked at http://documentcloud.github.com/backbone/#Collection-create,
http://documentcloud.github.com/backbone/docs/todos.html, http://liquidmedia.ca/blog/2011/01/backbone-js-part-1/, http://liquidmedia.ca/blog/2011/01/an-intro-to-backbone-js-part-2-controllers-and-views/ and https://gist.github.com/1655019, but I can't seem to find any answers there. I feel like I've done something stupid, but just can't see it!
It turns out, it was me not knowing how to extract json parameters in sinatra properly. From this site: http://mini.softwareas.com/posting-json-to-a-sinatra-mongodb-service, I found out I had to use request.body.read.to_s instead of the params hash ie,
post '/:team_id/team-members.json' do
request_body = JSON.parse(request.body.read.to_s)
team_member_name = request_body["name"]
# ...
end
I had the same problem. I am on PHP, though. Since Backbone sends POST data not in a query string, but rather in a plain JSON string, the data is not available thru $_POST. To read the Backbone POST data:
// the 'true' param returns an array rather than an object
$post = json_decode(file_get_contents('php://input'), true);
You can also read it directly from $HTTP_RAW_POST_DATA.