BackboneJs unable to render template in rendered template - templates

I have two views and I want to render a second one inside the first one.
I am doing this:
First view:
var EventContainer = Backbone.View.extend({
el: '#sectionContainer',
initialize: function() {
this.render();
},
render: function() {
var that = this;
$.get('templates/home/eventContainer.html', function(data) {
that.template = _.template(data, {}); //Option to pass any dynamic values to template
$(that.el).append(that.template); //adding the template content to the main template.
}, 'html');
new EventList();
return this;
}
});
Template for the first view:
<div class="main-content pull-left">
<ul id="eventList" class="event-list no-list-style no-padding"> </ul>
</div>
Second view:
var EventList = Backbone.View.extend({
el: '#eventList',
initialize: function() {
var that = this;
this.EventsCollection = new EventsCollection();
this.EventsCollection.fetch({
success: function(data) {
that.collection = data;
that.setTemplate();
},
error: function() {}
});
},
setTemplate: function() {
var that = this;
$.get('templates/home/eventList.html', function(data) {
that.template = _.template(data);
that.render();
}, 'html');
},
render: function() {
var that = this;
this.collection.each(function(eventData) {
var eventTemplate = that.template(eventData.toJSON());
console.log('---> ' + $(that.el));
$(that.el).append(eventTemplate);
});
return this;
}
});
Template for the second View:
<li class="event-item">
<small><%= like_count%> </small>
</li>
When I try to render the second view, el is undefined.
What m I doing wrong?
Thanks.

If you have a look at this.el inside EventList#initialize, I think you'll find that it is undefined rather than the <ul> that you're expecting it to be. Why would that be? Where does #eventList come from? #eventList comes from EventContainer's template and that won't be in the DOM until this AJAX call:
$.get('templates/home/eventContainer.html', ...)
completes. That $.get won't complete until well after your new EventList();. When you instantiate your EventList, it will go looking for #eventList but won't find anything, that will leave you with this.el being undefined. Then later on, that $.get will return and #eventList will show up in the DOM; this is why $('#eventList') works.
The easy solution is to defer instantiating your EventList until after you've added the necessary things to the DOM:
$.get('templates/home/eventContainer.html', function(data) {
that.template = _.template(data, {});
$(that.el).append(that.template);
new EventList(); // <-----------------------
}, 'html');
If you're going to do this, then you could make the dependency explicit by dropping the el from the EventList definition and specifying it when you create the view instead:
$.get('templates/home/eventContainer.html', function(data) {
that.template = _.template(data, {});
$(that.el).append(that.template);
new EventList({ el: that.$('#eventList') });
}, 'html');

Related

Delay ember view render till $getJSON isLoaded

The problem with this code is that the render code is entered twice, and the buffer is not where I expect it. Even when I get the buffer, the stuff I push in is not rendered to the screen.
App.FilterView = Ember.View.extend({
init: function() {
var filter = this.get('filter');
this.set('content', App.ViewFilter.find(filter));
this._super();
},
render: function(buffer) {
var content = this.get('content');
if(!this.get('content.isLoaded')) { return; }
var keys = Object.keys(content.data);
keys.forEach(function(item) {
this.renderItem(buffer,content.data[item], item);
}, this);
}.observes('content.isLoaded'),
renderItem: function(buffer, item, key) {
buffer.push('<label for="' + key + '"> ' + item + '</label>');
}
});
And the App.ViewFilter.find()
App.ViewFilter = Ember.Object.extend();
App.ViewFilter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: ''
});
$.getJSON("http://localhost:3000/filter/" + o, function(response) {
result.set('data', response);
result.set('isLoaded', true);
});
return result;
}
});
I am getting the data I expect and once isLoaded triggers, everything runs, I am just not getting the HTML in my browser.
As it turns out the answer was close to what I had with using jquery then() on the $getJSON call. If you are new to promises, the documentation is not entirely straight forward. Here is what you need to know. You have to create an object outside the promise - that you will return immediately at the end and inside the promise you will have a function that updates that object once the data is returned. Like this:
App.Filter = Ember.Object.extend();
App.Filter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: Ember.Object.create()
});
$.getJSON("http://localhost:3000/filter/" + o).then(function(response) {
var controls = Em.A();
var keys = Ember.keys(response);
keys.forEach(function(key) {
controls.pushObject(App.FilterControl.create({
id: key,
label: response[key].label,
op: response[key].op,
content: response[key].content
})
);
});
result.set('data', controls);
result.set('isLoaded', true);
});
return result;
}
});
Whatever the function inside then(), is the callback routine that will be called once the data is returned. It needs to reference the object you created outside the $getJSON call and returned immediately. Then this works inside the view:
didInsertElement: function() {
if (this.get('content.isLoaded')) {
var model = this.get('content.data');
this.createFormView(model);
}
}.observes('content.isLoaded'),
createFormView: function(data) {
var self = this;
var filterController = App.FilterController.create({ model: data});
var filterView = Ember.View.create({
elementId: 'row-filter',
controller: filterController,
templateName: 'filter-form'
});
self.pushObject(filterView);
},
You can see a full app (and bit more complete/complicated) example here

Backbone - How to create a nested list with subview list items?

I'm a Backbone noob and I've been at a standstill for 2 days now and can't figure out where I'm going wrong. Could anyone help me out?
My app is retrieving a JSON file with a list of components in it. Each component has a category it belongs to. I create a view called "Components" that is a collapsible list. When a component category is clicked, it should open up to show the components in that category. Each of these components (list items) a separate view called "Component".
I'm using a lot of append()'s in the parent view and I don't think this is efficient. I tried to compile a string of html and then append it to the view in one statement but the events of the subviews weren't triggering.
There are probably a few errors going on here. Even though my sublist items should be wrapped in ul's they aren't being. If someone can put me on the path to enlightenment I'd be really grateful!
Here's my code
/* ----------------- PARENT VIEW ---------------------- */
var ComponentsView = Backbone.View.extend({
id: 'components-view',
className: 'components-view',
html: [
'<div class="panel panel--components">',
'<h3 class="panel__heading">add an item</h3>',
'<ul class="component-list"></ul>',
'</div>'
].join(''),
initialize: function(){
var types = [];
var currentTypeSelected = 1;
this.getTypes = function(){
return types;
}
this.getCurrentTypeSelected = function(){
return currentTypeSelected;
}
this.setCurrentTypeSelected = function(value){
currentTypeSelected = value;
}
if(this.collection.length){
this.collection.each(function(model){
var thisItemType = model.attributes.type;
if(types.indexOf(thisItemType)==-1){
types.push(thisItemType);
}
});
}
this.$el.html(this.html);
this.$componentList = this.$('.component-list');
this.render();
},
render: function(){
var that = this;
this.getTypes().forEach(function(type){
that.$('.component-list').append('<li class="component-type">' + type + '');
// now cycle through all the componenets of this type
that.$('.component-list').append('<ul>');
that.collection.byType(type).each(function(model){
that.$('.component-list').append('<li class="component">');
that.$('.component-list').append(that.renderIndividualComponent(model));
that.$('.component-list').append('</li>');
});
that.$('.component-list').append('</ul>');
});
},
renderIndividualComponent: function(model){
var componentView = new ComponentView({model: model});
return componentView.$el;
},
events: {
'click .component-type': 'onOpenSubList'
},
onOpenSubList: function (e) {
alert('open sub list');
}
});
/* ----------------- SUB (list item) VIEW ---------------------- */
var ComponentView = Backbone.View.extend({
tagName: "li",
className: "component",
initialize: function(model){
this.render();
},
render: function(){
var html = '' + this.model.attributes.description + ''//template(this.model.attributes);
$(this.el).append(html);
return this;
},
events: {
'click a': 'onAddComponent'
},
onAddComponent: function (e) {
e.preventDefault();
alert('add component');
}
});

has no method 'toJSON' error in my view

I'm learning backbone.js and I'm building my first multimodule app. I'm getting an error that I've never seen before and I think I know the reason, but I can't see how to fix it. I believe it's because the model isn't actually available to the view yet, but I can't see why.
The error is:
Uncaught TypeError: Object function (){ return parent.apply(this, arguments); } has no method 'toJSON'
This is for line 11 in my view, msg.App.MessageListItemView.
Here's my model:
var msgApp = msgApp || {};
msgApp.Message = Backbone.Model.extend();
Here's my collection:
var msgApp = msgApp || {};
msgApp.MessageCollection = Backbone.Collection.extend({
model: msgApp.Message,
url: MESSAGES_API // Call to REST API with Tastypie
});
Here's my list view:
var msgApp = msgApp || {};
msgApp.MessageListView = Backbone.View.extend({
el: '#gps-app',
initialize: function() {
this.collection = new msgApp.MessageCollection();
this.collection.fetch({reset: true});
this.render();
this.listenTo( this.collection, 'reset', this.render );
},
// render messages by rendering each message in it's collection
render: function() {
this.collection.each(function(item){
this.renderMessage(item);
}, this);
},
// render a message by creating a MessageView and appending the the element it renders to the messages element
renderMessage: function(item) {
var messageView = new msgApp.MessageListItemlView({
model: msgApp.Message
});
this.$el.append(messageView.render().el);
}
});
Here's my item view:
var msgApp = msgApp || {};
msgApp.MessageListItemlView = Backbone.View.extend({
tagName: 'li',
className: 'message-list-item',
template: _.template($('#messageListItem').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
And here is my router:
var AppRouter = Backbone.Router.extend({
routes: {
'messages/': 'allMessages',
},
allMessages:function() {
this.messageList = new msgApp.MessageCollection();
this.messageListView = new msgApp.MessageListView({model:this.messageList});
console.log('I got to messages!');
},
});
var app_router = new AppRouter;
I'm looking for any and all suggestions. I'm a noob to begin with, and this is my first multimodule app so I'm having a little trouble managing scope I think.
Thanks for you time!
try to change model: msgApp.Message in msgApp.MessageListView like this:
// render a message by creating a MessageView and appending the the element it renders to the messages element
renderMessage: function(item) {
var messageView = new msgApp.MessageListItemlView({
model: item
});
this.$el.append(messageView.render().el);
}
model parameter in views don't expect type of model, but instance of some model. Hope this helps.

Swapping a ContainerView in and out of another ContainerView results in destroyed views

I have a ContainerView that swaps other views in and out. I use another ContainerView as content. Trying to swap in the nested ContainerView, after I swapped it out, results in the error: Uncaught Error: assertion failed: calling set on destroyed object.
Here's the fiddle: http://jsfiddle.net/hekevintran/bFSKD/
To make the error happen, click "Other Form" and then click "First Form".
I think the error is because views that are removed from ContainerViews are destroyed and the child views of the nested ContainerView are not recreated. What's the right way to fix this example?
Templates:
<script type="text/x-handlebars" data-template-name="box">
<div>
{{#each forms}}
<button {{action "selectForm" this }}>{{this.name}}</button>
{{/each}}
{{view container}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="form">
<form>
{{#each fields}}
<div>
{{this.label}}: {{view this.widget}}
</div>
{{/each}}
</form>
</script>
JavaScript:
App = Ember.Application.create({});
App.BoxController = Ember.Object.extend({
initialForm: null,
currentForm: null,
init: function () {
var form = this.get('initialForm');
this.set('currentForm', form);
this.get('container').set('currentView', form.get('view').create());
},
forms: [],
container: function () {
return Ember.ContainerView.create({
boxController: this,
controllerBinding: 'boxController.currentForm'
})
}.property(),
selectForm: function (form) {
this.set('currentForm', form);
this.get('container').set('currentView', form.get('view').create());
}
});
App.Field = Ember.Object.extend({
value: null,
widgetBaseClass: Ember.TextField,
widget: function () {
return this.get('widgetBaseClass').extend({
field: this,
valueBinding: 'field.value'
});
}.property('widgetBaseClass')
});
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
childViews: [field.get('select1').create(), field.get('select2').create()]
});
}.property('select1', 'select2'),
fromValue: null,
toValue: null,
value: function () {
return [this.get('fromValue.value'), this.get('toValue.value')];
}.property('fromValue', 'toValue'),
choices: [
'1',
'2',
'3',
'4'
],
remainingChoices: function () {
var fromValue = this.get('fromValue');
if (fromValue) {
var choices = this.get('choices');
var index = choices.indexOf(fromValue);
return choices.slice(index + 1);
}
return [];
}.property('fromValue', 'choices'),
select1: function () {
return Ember.Select.extend({
field: this,
valueBinding: 'field.fromValue',
contentBinding: 'field.choices'
});
}.property(),
select2: function () {
return Ember.Select.extend({
field: this,
valueBinding: 'field.toValue',
contentBinding: 'field.remainingChoices',
contentHasChangedOnce: false,
contentChanged: function () {
// Set the initial value only once
if (! this.get('contentHasChangedOnce')) {
this.set('contentHasChangedOnce', true);
this.set('value', this.get('content')[0]);
}
// Reset the value if the chosen value is no longer
// available
if (! this.get('content').contains(this.get('value'))) {
this.set('value', this.get('content')[0]);
}
}.observes('content')
});
}.property()
});
App.Form = Ember.Object.extend({
fieldNames: [],
fields: function () {
var that = this;
var out = [];
_.each(this.get('fieldNames'), function (fieldName) {
out.pushObject(that.get(fieldName));
});
return out;
}.property('fieldNames')
});
aForm = App.Form.create({
name: 'First Form',
fieldNames: [
'a',
'b'
],
a: App.Field.create({label: 'A'}),
b: App.RangeField.create({label: 'B'}),
view: Ember.View.extend({
templateName: 'form'
})
});
var boxController = App.BoxController.create({
initialForm: aForm,
forms: [
aForm,
Ember.Object.create({
name: 'Other Form',
view: Ember.View.extend({
template: Ember.Handlebars.compile('Foobar')
})
})
]
});
var boxView = Ember.View.create({
templateName: 'box',
controller: boxController
});
boxView.append();
The issue is that you are instantiating select1 and select2 when you create the class extending ContainerView inside the widget method of App.RangeField.
Change this code:
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
childViews: [field.get('select1').create(), field.get('select2').create()]
});
}.property('select1', 'select2'),
...
}
to this:
App.RangeField = App.Field.extend({
widget: function () {
var field = this;
return Ember.ContainerView.extend({
init: function() {
this.set('childViews', [field.get('select1').create(), field.get('select2').create()]);
this._super();
}
});
}.property('select1', 'select2'),
...
}
Now you create new child views each time you instantiate widget instead of re-using the same two views which were destroyed the first time you removed them from the DOM.

Dojo Widget Templates

With reference to Simple Login implementation for Dojo MVC / - there is one point i don't understand. With regards to sample from phusick, the login dialog class does a call of dom.byId("dialog-template") - "dialog-template" is an id from the script which is the template for the dialog and should be present in an html template - not in the main html. So if I remove that, the call to dom.byId would fail
so my code structure is as follows
main.html ( calls Only main.js is called - nothing more)
main.js ( Contains the following)
require([
"dojo/_base/declare","dojo/_base/lang","dojo/on","dojo/dom","dojo/Evented",
"dojo/_base/Deferred","dojo/json","dijit/_Widget","dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin","dijit/Dialog",
"widgets/LoginDialog",
"widgets/LoginController",
"dijit/form/Form","dijit/form/ValidationTextBox","dijit/form/Button",
"dojo/domReady!"
], function(
declare,lang,on,dom,Evented,Deferred,JSON,
_Widget,
_TemplatedMixin,
_WidgetsInTemplateMixin,
Dialog,
LoginDialog,
LoginController
) {
// provide username & password in constructor
// since we do not have web service here to authenticate against
var loginController = new LoginController({username: "user", password: "user"});
var loginDialog = new LoginDialog({ controller: loginController});
loginDialog.startup();
loginDialog.show();
loginDialog.on("cancel", function() {
console.log("Login cancelled.");
});
loginDialog.on("error", function() {
console.log("Login error.");
});
loginDialog.on("success", function() {
console.log("Login success.");
console.log(JSON.stringify(this.form.get("value")));
});
});
Now LoginDialog.js and LoginDialogTemplate.html is the templatised widget for the dialog
and LoginController.js is the controller.
My LoginDialog.js is
define([
"dojo/_base/declare","dojo/_base/lang","dojo/on","dojo/dom","dojo/Evented","dojo/_base/Deferred","dojo/json",
"dijit/_Widget","dijit/_TemplatedMixin","dijit/_WidgetsInTemplateMixin",
"dijit/Dialog","dijit/form/Form","dijit/form/ValidationTextBox","dijit/form/Button",
"dojo/text!templates/loginDialogTemplate.html",
"dojo/text!templates/loginFormTemplate.html",
"dojo/domReady!"
], function(
declare,lang,on,dom,Evented,Deferred,JSON,
_Widget,
_TemplatedMixin,
_WidgetsInTemplateMixin,
Dialog,
Form,
Button,
template
) {
return declare([ Dialog, Evented], {
READY: 0,
BUSY: 1,
title: "Login Dialog",
message: "",
busyLabel: "Working...",
// Binding property values to DOM nodes in templates
// see: http://www.enterprisedojo.com/2010/10/02/lessons-in-widgetry-binding-property-values-to-dom-nodes-in-templates/
attributeMap: lang.delegate(dijit._Widget.prototype.attributeMap, {
message: {
node: "messageNode",
type: "innerHTML"
}
}),
constructor: function(/*Object*/ kwArgs) {
lang.mixin(this, kwArgs);
var dialogTemplate = dom.byId("dialog-template").textContent;
var formTemplate = dom.byId("login-form-template").textContent;
var template = lang.replace(dialogTemplate, {
form: formTemplate
});
var contentWidget = new (declare(
[_Widget, _TemplatedMixin, _WidgetsInTemplateMixin],
{
templateString: template
}
));
contentWidget.startup();
var content = this.content = contentWidget;
this.form = content.form;
// shortcuts
this.submitButton = content.submitButton;
this.cancelButton = content.cancelButton;
this.messageNode = content.messageNode;
},
postCreate: function() {
this.inherited(arguments);
this.readyState= this.READY;
this.okLabel = this.submitButton.get("label");
this.connect(this.submitButton, "onClick", "onSubmit");
this.connect(this.cancelButton, "onClick", "onCancel");
this.watch("readyState", lang.hitch(this, "_onReadyStateChange"));
this.form.watch("state", lang.hitch(this, "_onValidStateChange"));
this._onValidStateChange();
},
onSubmit: function() {
this.set("readyState", this.BUSY);
this.set("message", "");
var data = this.form.get("value");
var auth = this.controller.login(data);
Deferred.when(auth, lang.hitch(this, function(loginSuccess) {
if (loginSuccess === true) {
this.onLoginSuccess();
return;
}
this.onLoginError();
}));
},
onLoginSuccess: function() {
this.set("readyState", this.READY);
this.set("message", "Login sucessful.");
this.emit("success");
},
onLoginError: function() {
this.set("readyState", this.READY);
this.set("message", "Please try again.");
this.emit("error");
},
onCancel: function() {
this.emit("cancel");
},
_onValidStateChange: function() {
this.submitButton.set("disabled", !!this.form.get("state").length);
},
_onReadyStateChange: function() {
var isBusy = this.get("readyState") == this.BUSY;
this.submitButton.set("label", isBusy ? this.busyLabel : this.okLabel);
this.submitButton.set("disabled", isBusy);
}
});
});
My loginDialogTemplate.html is as follows
<script type="text/template" id="dialog-template">
<div style="width:300px;">
<div class="dijitDialogPaneContentArea">
<div data-dojo-attach-point="contentNode">
{form}
</div>
</div>
<div class="dijitDialogPaneActionBar">
<div
class="message"
data-dojo-attach-point="messageNode"
></div>
<button
data-dojo-type="dijit.form.Button"
data-dojo-props=""
data-dojo-attach-point="submitButton"
>
OK
</button>
<button
data-dojo-type="dijit.form.Button"
data-dojo-attach-point="cancelButton"
>
Cancel
</button>
</div>
</div>
</script>
Since the template has the id="dialog-template" so I guess when the widget calls the dom.byId("dialog-template"), it throws an error "TypeError: dom.byId(...) is null" at the line :-> var dialogTemplate = dom.byId("dialog-template").textContent;
So what am I doing wrong here?
If i use all the template scripts in the main html it works fine.
Asif,
Since you're passing in the templates in the define function, you don't need the dom.byId() to get the content. Try this:
Remove the elements from your HTML templates.
In LoginDialog.js, change your function arguments to:
...
Button,
dialogTemplate,
formTemplate
You'll need the formTemplate for the next change. I used 'dialogTemplate' instead of your 'template' so it's more obvious how it's replacing the code from the example. Next, change the beginning of the constructor to:
constructor: function(/*Object*/ kwArgs) {
lang.mixin(this, kwArgs);
//var dialogTemplate = dom.byId("dialog-template").textContent;
//var formTemplate = dom.byId("login-form-template").textContent;
var template = lang.replace(dialogTemplate, {
form: formTemplate
});
var contentWidget = new (declare(
...
I only left the commented code in so you can see what I changed. What it does is create a new template string called 'template' by substituting the {form} placeholder in your dialogTemplate HTML with the formTemplate you passed in. Then it's using that new template string to create the widget.