Connecting predefined HTML to Models and Views in Backbone - javascript

I'm starting out with Backbone.js so I must say I'm not yet very familiar with the concepts.
I have predefined HTML and I want to use Backbone to manage this. This is important and I want to keep it like this.
Say this is (part of) my HTML:
<div class="pig" data-id="1">
<h1>Harry</h1>
<input type="text" value="Harry">
</div>
<div class="pig" data-id="2">
<h1>Jill</h1>
<input type="text" value="Jill">
</div>
<div class="pig" data-id="3">
<h1>Bob</h1>
<input type="text" value="Bob">
</div>
Now the idea is that when you change the input this should update my Backbone Model and render the view, resuling in the h1 being updated with the new name. I'm not sure how I should set up my models and views.
I kind of have the structure of my models and my views, but I don't know how I should use them.
At the moment I have something like this:
var PigModel = Backbone.Model.extend()
var pigs = new PigModel()
pigs.reset([
{"id": "1", "name": "Harry"},
{"id": "2", "name": "Jill"},
{"id": "3", "name": "Bob"}
])
var PigView = Backbone.View.extend({
el: '.pig',
events: {
'change input': function() {
// update appropriate Model ?
this.render()
}
},
render: function() {
var new_name = // get new name from model ?
var pig_template = _.template($('#pig_template').html())
var new_contents = pig_template({ name: new_name })
// render only the pig that was changed ?
}
})
So at the places where I added comments in the code I don't know what to do:
At the change event, how do I find the model that belongs to this view? I could loop through all the models but that seems inefficient.
Again in the render method, how do I get the model that belongs to this view?
In the render method, how can I only render the pig view the event was on, and not render all the pigs?
Maybe I'm going for a total wrong approach over here. If so, please point me in the right direction.

Ok, I managed to figure it out.
The idea is to loop through your existing HTML using jQuery, then creating instances of views and models of it using the jquery selectors and the preloaded json.
HTML:
<div class="pig">
<h1>Harry</h1>
<input type="text" value="Harry" />
</div>
<div class="pig">
<h1>Jill</h1>
<input type="text" value="Jill" />
</div>
<div class="pig">
<h1>Bob</h1>
<input type="text" value="Bob" />
</div>
Javascript:
$(function() {
var PigModel = Backbone.Model.extend()
var PigView = Backbone.View.extend({
events: {
'change input': function(e) {
this.model.set('name', e.currentTarget.value)
this.render()
}
},
render: function() {
this.$el.html(
'<h1>' + this.model.get('name') + '</h1>' +
'<input type="text" value="' + this.model.get('name') + '" />'
)
}
})
var pig_data = [
{"name": "Harry"},
{"name": "Jill"},
{"name": "Bob"}
]
// the magic starts here
var pig_number = 0
$('.pig').each(function() {
new PigView({
el: this,
model: new PigModel(pig_data[pig_number])
})
})
})
Jsfiddle: http://jsfiddle.net/tU3Mt/1/
Like this I can serve a complete HTML page from the server, then load the desired elements into my backbone views/models and manage them from there.
About wether this is the way backbone should be used or not:
It may not be the most logical/efficient way to do this from a developer point of view. However, I think this approach has some important benefits:
The client get's HTML served directly and won't have to process any javascript before the page can be presented. This will become more noticable as your HTML gets bigger/more complex.
Search engines/web crawlers can read your page because it serves the complete HTML.
For some people these points may not be that important, because a webapp will be fast after it has been loaded and search engines won't have to crawl it. However in some situations you might have a mix of a website and a webapp, where the page needs to load fast, be crawlable but also have a responsive interface which is complex enough to make a developer want to use something like backbone. If somebody has other ideas about this I sure like to hear them.

I would take a little bit of a different approach. Create a collection of Pig models. Have a view that knows how to render a single pig, PigView. Let it be responsible for updating the one pig and it's corresponding h1. Have a higher level view that renders the collection of pigs into whatever parent element you want.
var PigModel = Backbone.Model.extend()
// You want a collection of pig models
var pigs = new Backbone.Collection([
{"id": "1", "name": "Harry"},
{"id": "2", "name": "Jill"},
{"id": "3", "name": "Bob"}
], {model: PigModel});
var PigView = Backbone.View.extend({
// Want to set the class for the generated el not pass a selector
className : "pig",
// Dummy template from your markup in question
template : function (modelAttrs) {
return "<h1>"+modelAttrs.name+"</h1><input type='text' value='"+modelAttrs.name+"'>";
},
events: {
'change input': function(e) {
// You have a reference to the model through this
this.model.set({name : e.currentTarget.value});
this.render();
}
},
// Bare bones render
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Parent view for responsible for rendering the collection of pigs somewhere.
var HigherLevelView = Backbone.View.extend({
render : function () {
pigs.each(function (pig) {
this.$el.append(new PigView({model : pig}).render().el);
});
}
});
$("body").append(new HigherLevelView().render().el);
Fiddle

Attach to existing html is possible, but it is not very common approach. Because of that, there is almost no real info on this :(
You could try to follow steps described in following article: http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-2-progressive-enhancement-with-backbone-js/
Idea is, to add some data-attribute into rendered html, so you will be able to match model with rendered html.

Related

Javascript templating solution that allows after-rendering use of injected objects?

So, I'm building an application based on Backbone.js, by using templates for rendering some objects.
It's working, however now I need to dynamically reference the objects at runtime, and I'm not sure it's possible with the templating solutions I've seen (underscore, handlebar, ...), that "flatten" the javascript.
To illustrate, I have a list of objects, let's say Tasks.
I have a model that can be simplified as such :
{{#each tasks.models as |task|}}
<div>
{{task.name}}
</div>
{{/each}}
Now, I will need to use the 'task' object dynamically, after the rendering is finished. For example, do something like this :
<div>
{{task.name}} - <button onClick="task.setComplete()" />
</div>
Of course this way doesn't work ; and neither do something like {{task}}.setComplete(), as {{task}} is transformed to a string when rendering.
Is there a way to do this?
I was thinking I need closures to keep the objects, the only way to obtain them is not to flatten the html, as everything is transformed to string otherwise.
Any idea? Maybe are there templating libraries that would allow to generate directly DOM objects, that I could add to my document ?
Thanks in advance,
This question is tagged with backbone.js so you should use Backbone's normal view event handling system instead of onclick handlers. You mention tasks.models so presumably tasks is a collection.
One approach would be to use data-attributes to stash the model ids. Your template would look like this:
{{#each tasks}}
<div>
{{name}} - <button data-id="{{id}}" type="button">Completed</button>
</div>
{{/each}}
and then your view would be set up like this:
Backbone.View.extend({
events: {
'click button': 'completed'
},
render: function() {
var t = Handlebars.compile($('#whatever-the-template-is').html());
this.$el.append(t({
tasks: this.collection.toJSON()
}));
return this;
},
completed: function(ev) {
var id = $(ev.currentTarget).data('id');
var m = this.collection.get(id);
// Do whatever needs to be done to the model `m`...
}
});
Demo: https://jsfiddle.net/ambiguous/z7go5ubj/
All the code stays in the view (where all the data is already) and the template only handles presentation. No globals, nice separation of concerns, and idiomatic Backbone structure.
If the per-model parts of your view are more complicated then you could have one view for the collection and subviews for each model. In this case, your per-model templates would look like this:
<div>
{{name}} - <button type="button">Completed</button>
</div>
No more data-attribute needed. You'd have a new per-model view something like this:
var VM = Backbone.View.extend({
events: {
'click button': 'completed'
},
render: function() {
var t = Handlebars.compile($('#whatever-the-template-is').html());
this.$el.append(t(this.model.toJSON()));
return this;
},
completed: function() {
console.log('completed: ', this.model.toJSON());
}
});
and the loop would move to the collection view:
var VC = Backbone.View.extend({
render: function() {
this.collection.each(function(m) {
var v = new VM({ model: m });
this.$el.append(v.render().el);
}, this);
return this;
}
});
Demo: https://jsfiddle.net/ambiguous/5h5gwhep/
Of course in real life your VC would keep track of its VMs so that VC#remove could call remove on all its child VMs.
My answer is just a hint, I tried to keep close to the question. Refer to this answer for a better solution: https://stackoverflow.com/a/32493586/1636522.
You could use the index or any other information to find the item. Here is an example using Handlebars, assuming each task can be identified by an id:
var tasks = [];
var source = $('#source').html();
var template = Handlebars.compile(source);
tasks[0] = { id: 42, label: 'coffee', description: 'Have a cup of coffee.' };
tasks[1] = { id: 13, label: 'help', description: 'Connect to StackOverflow.' };
tasks[2] = { id: 40, label: 'smile', description: 'Make μ smile.' };
$('#placeholder').html(template({ tasks: tasks }));
function onClick (id) {
var task, i = 0, l = tasks.length;
while (i < l && tasks[i].id !== id) i++;
if (i === l) {
// not found
}
else {
task = tasks[i];
alert(task.label + ': ' + task.description);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.2/handlebars.min.js"></script>
<script id="source" type="text/x-handlebars-template">
{{#each tasks}}
<button
type="button"
class="task"
style="margin-right:.5em"
onclick="onClick({{id}})"
>{{label}}</a>
{{/each}}
</script>
<div id="placeholder"></div>

Marionette.js - can I detect onAppend?

I have a silly problem, where my only solution is a sloppy hack that is now giving me other problems.
See my fiddle,
or read the code here:
HTML:
<input id='1' value='input1' />
<template id='template1'>
<input id='2' value='input2' />
</template>
JS - Item View Declaration:
// Declare an ItemView, a simple input template.
var Input2 = Marionette.ItemView.extend({
template: '#template1',
onRender: function () {
console.log('hi');
},
ui: { input2: '#2' },
onRender: function () {
var self = this;
// Despite not being in the DOM yet, you can reference
// the input, through the 'this' command, as the
// input is a logical child of the ItemView.
this.ui.input2.val('this works');
// However, you can not call focus(), as it
// must be part of the DOM.
this.ui.input2.focus();
// So, I have had to resort to this hack, which
// TOTALLY SUCKS.
setTimeout(function(){
self.ui.input2.focus();
self.ui.input2.val('Now it focused. Dammit');
}, 1000)
},
})
JS - Controller
// To start, we focus input 1. This works.
$('#1').focus();
// Now, we make input 2.
var input2 = new Input2();
// Now we 1. render, (2. onRender is called), 3. append it to the DOM.
$(document.body).append(input2.render().el);
As one can see above, my problem is that I can not make a View call focus on itself after it is rendered (onRender), as it has not yet been appended to the DOM. As far as I know, there is no other event called such as onAppend, that would let me detect when it has actually been appended to the DOM.
I don't want to call focus from outside of the ItemView. It has to be done from within for my purposes.
Any bright ideas?
UPDATE
Turns out that onShow() is called on all DOM appends in Marionette.js, be it CollectionView, CompositeView or Region, and it isn't in the documentation!
Thanks a million, lukaszfiszer.
The solution is to render your ItemView inside a Marionette.Region. This way an onShow method will be called on the view once it's inserted in the DOM.
Example:
HTML
<input id='1' value='input1' />
<div id="inputRegion"></div>
<template id='template1'>
<input id='2' value='input2' />
</template>
JS ItemView
(...)
onShow: function () {
this.ui.input2.val('this works');
this.ui.input2.focus();
},
(...)
JS Controller
$('#1').focus();
var inputRegion = new Backbone.Marionette.Region({
el: "#inputRegion"
});
var input2 = new Input2();
inputRegion.show(input2);
More information in Marionette docs: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#region-events-and-callbacks
Well, I managed to solve it by extending Marionette.js, but if anyone else has a better idea that doesn't involve extending a library, I will GLADLY accept it and buy you a doughnut.
// After studying Marionette.js' annotated source code,
// I found these three functions are the only places
// where a view is appended after rendering. Extending
// these by adding an onAppend call to the end of
// each lets me focus and do other DOM manipulation in
// the ItemView or Region, once I am certain it is in
// the DOM.
_.extend(Marionette.CollectionView.prototype, {
appendHtml: function(collectionView, itemView, index){
collectionView.$el.append(itemView.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.CompositeView.prototype, {
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.Region.prototype, {
open: function(view){
this.$el.empty().append(view.el);
if (view.onAppend) { view.onAppend() }
},
});

My fake case statement in Emberjs is blowing up. What's the right way?

I'm trying to render a different handlebars template based on the current value of a property in my model, and there could be quite a few options (hence I'd rather not use a lot of {{#if}}s). The best thing I can think of is this:
Ember.Handlebars.registerBoundHelper('selectorType', function(name, options) {
return Ember.Handlebars.compile("{{template _selectors_" + name + "}}")(options.contexts[0], options);
});
And I use that in my template like:
{{selectorType selector.name}}
(instead of like a hundred {{#if}}s)
The problem is that I get this error during render: "You can't use appendChild outside of the rendering process"
Clearly I'm doing something wrong. What's the right way to do this?
I don't think there's any need to create a helper to do this. You can do it from within the view by modifying the templateName and then calling the rerender method once you've changed its templateName:
init: function() {
this.set('templateName', 'firstOne');
this._super();
},
click: function() {
this.set('templateName', 'secondOne');
this.rerender();
}
We can use the init method for setting the empty templateName before the template has been rendered. We'll then call the _super method to complete the insertion of the view into the DOM. We can then trigger the change of the view on the click event. We update the templateName variable and then call rerender() to re-render this particular view.
I've set you up a JSFiddle as an example: http://jsfiddle.net/pFkaE/ try clicking on "First One." to change the view to the secondOne.
I ended up solving this using a ContainerView with dynamic childViews, see Ember.js dynamic child views for a discussion on how.
The relevant code is (coffeescript):
App.SelectorType = Ember.Object.extend
name: null
type: null
typeView: null
options: null
App.SelectorTypes = [
App.SelectorType.create(
name: 'foo'
type: 'bar'
) #, more etc
]
App.SelectorTypes.forEach (t) ->
t.set 'typeView', Ember.View.create
templateName: "selectors/_#{t.get('viewType')}_view"
name: t.get('name')
App.SelectorDetailView = Ember.ContainerView.extend
didInsertElement: ->
#updateForm()
updateForm: (->
type = #get('type')
typeObject = App.SelectorTypes.findProperty('type', type)
return if Ember.isNone(type)
view = typeObject.get('typeView')
#get('childViews').forEach (v) -> v.remove()
#get('childViews').clear()
#get('childViews').pushObject(view)
).observes('type')
And the template:
Selector Type:
{{view Ember.Select
viewName=select
contentBinding="App.SelectorTypes"
optionValuePath="content.type"
optionLabelPath="content.name"
prompt="Pick a Selector"
valueBinding="selector.type"
}}
<dl>
<dt><label>Details</label></dt>
<dd>
{{view App.SelectorDetailView typeBinding="selector.type"}}
</dd>
</dl>
Seems too hard, though, would be interested to see better solutions!

Unable to bind a Kendo Mobile ListView to data using "data-bind=source:"

I am trying to bind the listview to the ViewModel. I have placed some hard coded data into the code to ensure that it is not a problem with the web services. I am not seeing any console errors so I am at a loss for how to troubleshoot this problem.
Ideally I would want to have as much of the code dealing with getting the data in the ViewModel, and I want to stay as close as possible to the way that you are supposed to use the KendoUI Mobile framework.
Html
<div data-role="view" id="contactView" data-model="ContactViewModel" data-init="dataInit">
<h1 id="ContactHallo">Contact Screen</h1>
<ul id="contactDetailList"
data-role="listview"
data-style="inset"
data-template="contactDetailtemplate"
data-bind="source: rdata">
</ul>
</div>
JavaScript
var ContactViewModel = kendo.observable({
rdata: null,
loadData: function () {
var testData = [
{
AssociatedContactType: "n\/a",
AssociatedProperties: [],
EmailAddress: "n\/a",
FName: "User1",
HomeNumber: "n\/a",
LName: "LastName",
MobileNumber: "+27 21 0823219213",
WorkNumber: "n\/a"
}];
var loadedData = new kendo.data.DataSource.create({ data: testData });
loadedData.read();
this.rdata = loadedData;
}
});
function dataInit() {
ContactViewModel.loadData();
}
var app = new kendo.mobile.Application($(document.body));
Template
<div>
<h4>Added Record</h4>
#:data.MobileNumber#
</div>
It would be interesting to know why someone down-voted the original question..
I cover this in one of my blog posts here: Kendo Mobile Gotchas, Tips, and Tricks.
The MVVM data bind actually happens before the init event, so your ContactViewModel.rdata is still null when the bind happens. However, I think if you properly call .set() when setting rdata, it might fix your issue:
loadData: function () {
...
this.set('rdata', loadedData);
}
The set should trigger the ListView to update since rdata is being set.
If that doesn't work, then you can get really tricky and delay the MVVM data bind until the init event by doing it yourself instead of using data-model declaratively.
To do that, you would remove data-model= attribute from your view, and instead manually call kendo.bind() at the end of your init function, like this:
<div data-role="view" id="contactView" data-init="dataInit">
function dataInit(initEvt) {
ContactViewModel.loadData();
kendo.bind($(initEvt.view.element), ContactViewModel, kendo.mobile.ui);
}

Backbone.js backed list not being refreshed by jQuery mobile (listview(‘refresh’))

I’m trying to add sort options to a JQM list which is backed by a backbone.js collection. I’m able to sort the collection (through the collection’s view) and rerender the list, but JQM isn’t refreshing the list.
I’ve been searching and I found several questions similar to mine (problems getting the JQM listview to refresh) but I’ve been unable to get it to work.
I’ve tried calling $(‘#list’).listview(‘refresh’) and $(‘#list-page’).page() etc. to no avail. I suspect that Perhaps I’m calling the refresh method in the wrong place (to early), but I’m not sure where else I should put it (I’m just starting out with backbone).
Here’s the markup and js.
HTML:
<div data-role="page" id="Main">
<div data-role="header"><h1>Main Page</h1></div>
<div data-role="content">
<ul data-role="listview">
<li>Page 1</li>
</ul>
</div>
<div data-role="footer"><h4>Footer</h4></div>
</div>
<div data-role="page" id="Page1">
<div data-role="header">
Back
<h1>Items</h1><a href="#dvItemSort" >Sort</a></div>
<div data-role="content">
<div id="dvTest">
<ul id="ItemList" data-role="listview" data-filter="true"></ul>
</div>
</div><div data-role="footer"><h4>Footer</h4></div></div>
<div data-role="page" id="dvItemSort">
<div data-role="header"><h4>Sort</h4></div>
<a href="#Page1" type="button"
name="btnSortByID" id="btnSortByID">ID</a>
<a href="#Page1" type="button"
name="btnSortByName" id="btnSortByName">Name </a>
</div>
Javascript:
$(function () {
window.Item = Backbone.Model.extend({
ID: null,
Name: null
});
window.ItemList = Backbone.Collection.extend({
model: Item
});
window.items = new ItemList;
window.ItemView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
this.model.bind('change', this.render, this);
},
render: function () {
$(this.el).html('<a>' + this.model.get('Name') + '</a>');
return this;
}
});
window.ItemListView = Backbone.View.extend({
el: $('body'),
_ItemViews: {},
events: {
"click #btnSortByID": "sortByID",
"click #btnSortByName": "sortByName"
},
initialize: function () {
items.bind('add', this.add, this);
items.bind('reset', this.render, this);
},
render: function () {
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
// $('#ItemList').listview();
// $('#Page1').trigger('create');
// $('#Page1').page(); //also doesn't work
},
add: function (item) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el);
},
sortByName: function () {
items.comparator = function (item) { return item.get('Name'); };
items.sort();
},
sortByID: function () {
items.comparator = function (item) { return item.get('ID'); };
items.sort();
}
});
window.itemListView = new ItemListView;
window.AppView = Backbone.View.extend({
el: $('body'),
initialize: function () {
items.add([{ID: 1, Name: 'Foo 1'}, {ID:2, Name: 'Bar 2'}]);
},
});
window.App = new AppView;
});
EDIT: I realized that the first line of html markup I posted wasn't displaying in my post so I pushed it down a line.
EDIT 2: Here's a link to a jsfiddle of the code http://jsfiddle.net/8vtyr/2/
EDIT 3 Looking at the resulting markup, it seems like JQM adds some of the classes to the list items. I tried adding them manually using a flag to determine whether the list was being reRendered as a result of a sort and the list then displays correctly.
However, besides being somewhat of an ugly solution, more importantly my backbone events on the “item” view no longer fire (in the code example I posted I didn’t put the code for the events because I was trying to keep it as relevant as possible).
EDIT 4 I sort of got it working by clearing my cache of views and recreating them. I posted my answer below.
EDIT 5
I updated my answer with what i think is a better answer.
I'm not sure if this should be its own answer or not (i did look through the FAQ a bit), so for now I’m just updating my previous answer.
I have now found a better way to sort the list using my cached views. Essentially the trick is to sort the collection, detach the elements from the DOM and then reattach them.
So
The code now would be
$list = $('#ItemList')
$('li', $list ).detach();
var frag = document.createDocumentFragment();
var view;
_.each(item.models, function (mdl) {
view = this._ItemViews[item.get('ID')];
frag.appendChild(view.el);
},this);
$list.append(frag);
OLD ANSWER
I sort of solved the problem. I was examing the rendered elements and I noticed that when the elements were “rerendered” (after the sort) they lost the event handlers (I checked in firebug). So I decided to clear my cache of views and recreate them. This seems to do the trick, though I’m not really sure why exactly.
For the code:
Instead of:
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
I clear the view cache and recreate the views.
$('#ItemList').empty();
this._ItemViews = {};
_.each(items.models, function (item, idx) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el)
}, this);
$('#ItemList').listview('refresh'); //works now
I think it would probably be better if I didn’t need to regenerate the cache, but at least this is a working solution and if I don't get a better answer then I'll just accept this one.
I had some luck in solving this, but the reason remains obscure to me.
Basically, at the top of my render view after establishing the html() of my element, I call listview(). Then, any further items I might add to a list call listview('refresh').

Categories