Backbone View Attribute set for next Instantiation? - javascript

I have a view that has a tooltip attribute. I want to set that attribute dynamically on initialize or render. However, when I set it, it appears on the next instantiation of that view instead of the current one:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {},
template : _.template(workoutSectionPreviewElement),
initialize : function() {
// this.setDetailsTooltip(); // doesn't work if run here either
},
setDetailsTooltip : function() {
// build details
...
// set tooltip
this.attributes['tooltip'] = details.join(', ');
},
render: function() {
this.setDetailsTooltip(); // applies to next WorkoutViewSlide
// build firstExercises images
var firstExercisesHTML = '';
for(key in this.model.workoutExerciseList.models) {
// stop after 3
if(key == 3)
break;
else
firstExercisesHTML += '<img src="' +
(this.model.workoutExerciseList.models[key].get("finalThumbnail") ?
this.model.workoutExerciseList.models[key].get("finalThumbnail").url : Exercise.SRC_NOIMAGE) + '" />';
}
// render the section slide
$(this.el).html(this.template({
workoutSection : this.model,
firstExercisesHTML : firstExercisesHTML,
WorkoutSection : WorkoutSection,
Exercise : Exercise
}));
return this;
}
});
Here is how I initialize the view:
// section preview
$('#sectionPreviews').append(
(new WorkoutSectionPreview({
model: that.workoutSections[that._renderWorkoutSectionIndex]
})).render().el
);
How can I dynamically set my attribute (tooltip) on the current view, and why is it affecting the next view?
Thanks

You can define attribute property as a function that returns object as result. So you're able to set your attributes dynamically.
var MyView = Backbone.View.extend({
model: MyModel,
tagName: 'article',
className: 'someClass',
attributes: function(){
return {
id: 'model-'+this.model.id,
someAttr: Math.random()
}
}
})
I hope it hepls.

I think your problem is right here:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {} // <----------------- This doesn't do what you think it does
Everything that you put in the .extend({...}) ends up in WorkoutSectionSlide.prototype, they aren't copied to the instances, they're shared by all instances through the prototype. The result in your case is that you have one attributes object that is shared by all WorkoutSectionSlides.
Furthermore, the view's attributes are only used while the the object is being constructed:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
The _ensureElement call is the thing that uses attributes and you'll notice that it comes before initialize is called. That order combined with the prototype behavior is why your attribute shows up on the next instance of the view. The attributes is really meant for static properties, your this.$el.attr('tooltip', ...) solution is a good way to handle a dynamic attribute.

Related

How to create customized list item with checkbox?

I've created a qooxdoo list with customized items containing checkbox and label.
My problem is: when I check the check box, it gets bigger which gives an ugly user experience. Also when I check some first items and scroll down, I see many items checked which should be unchecked by default.
Here's the code that someone can paste into play ground for qooxdoo:
// Create a button
var button1 = new qx.ui.form.Button("click to see list!", "icon/22/apps/internet-web-browser.png");
// Document is the application root
var doc = this.getRoot();
// Add button to document at fixed coordinates
doc.add(button1,
{
left : 100,
top : 50
});
var popup;
// Add an event listener
button1.addListener("execute", function(e) {
if (!popup) {
popup = new myApp.list();
}
popup.placeToWidget(button1);
popup.show();
});
/*
* class: list inside popup.
*/
qx.Class.define("myApp.list",
{
extend : qx.ui.popup.Popup,
construct : function()
{
this.base(arguments);
this.__createContent();
},
members : {
__createContent : function(){
this.set({
layout : new qx.ui.layout.VBox(),
minWidth : 300
});
//prepare data
var zones = [];
for (var i=0; i<100; i++){
zones.push({"LZN" : "ZONE " + i, "isChecked" : false});
}
var lstFences = new qx.ui.list.List();
this.add(lstFences, {flex : 2});
var delegate = {
createItem : function() {
return new myApp.customListItem();
},
bindItem : function(controller, item, id) {
controller.bindProperty("isChecked", "isChecked", null, item, id);
controller.bindPropertyReverse("isChecked", "isChecked", null, item, id);
controller.bindProperty("LZN", "LZN", null, item, id);
}
};
lstFences.setDelegate(delegate);
lstFences.setModel(qx.data.marshal.Json.createModel(zones));
lstFences.setItemHeight(50);
}
}
})
/**
* The custom list item
*/
qx.Class.define("myApp.customListItem", {
extend : qx.ui.core.Widget,
properties :
{
LZN:
{
apply : "__applyLZN",
nullable : true
},
isChecked :
{
apply : "__applyChecked",
event : "changeIsChecked",
nullable : true
}
},
construct : function()
{
this.base(arguments);
this.set({
padding : 5,
decorator : new qx.ui.decoration.Decorator().set({
bottom : [1, "dashed","#BBBBBB"]
})
});
this._setLayout(new qx.ui.layout.HBox().set({alignY : "middle"}));
// create the widgets
this._createChildControl(("isChecked"));
this._createChildControl(("LZN"));
},
members :
{
// overridden
_createChildControlImpl : function(id)
{
var control;
switch(id)
{
case "isChecked":
control = new qx.ui.form.CheckBox();
control.set({
padding : 5,
margin : 8,
value : false,
decorator : new qx.ui.decoration.Decorator().set({
width : 2,
color : "orange",
radius : 5
})
});
this._add(control);
break;
case "LZN":
control = new qx.ui.basic.Label();
control.set({allowGrowX : true});
this._add(control, {flex : 2});
break;
}
return control || this.base(arguments, id);
},
__applyLZN : function(value, old) {
var label = this.getChildControl("LZN");
label.setValue(value);
},
__applyChecked : function(value, old)
{
var checkBox = this.getChildControl("isChecked");
console.log(value, old);
checkBox.setValue(value);
}
}
});
There are two problems here:
The first one is the fact that by creating the checkbox as a subwidget via _createChildControlImpl makes the checkbox loosing its appearance (in sense of qooxdoo theme appearance) leading to the lost minWidth attribute which makes the checkbox having a width of 0 when unchecked and a width which is needed to show the check mark when it's checked. The solution here is to add an appearance to the myApp.customListItem class like this:
properties : {
appearance: {
refine : true,
init : "mycustomlistitem"
}
}
and afterward add a corresponding appearance to your theme:
appearances :
{
"mycustomlistitem" : "widget",
"mycustomlistitem/isChecked" : "checkbox"
}
You could also add all the styling you've done when instantiating the checkboxes (orange decorator etc.) within the appearance definition.
The second problem is that you’ve defined only a one way binding between the checkbox subwidget of the custom list item and its "isChecked" sub widget. You need a two way binding here, thus if the value of the property "isChanged" changes it’s value it prpoagates that to the checkbox and vice versa.
I've modified your playground sample accordingly by creating the missing appearance on the fly and by creating a two way binding between the checkbox and the list items “isChecked” property. Note that I've created the list directly in the app root for simplicity:
https://gist.github.com/level420/4662ae2bc72318b91227ab68e0421f41

Applying an addEventListener in a loop

This is my first question ever thus I apologize in advance might I use the wrong netiquette.
I'm exploring different solutions to implement a two way binding using Javascript only, I ran across the 'common mistake' when using a Closure inside a for loop hence having the counter variable always set on the last item, I found the explanation (and the solution) on this very site but then I came across a different issue I'd appreciate some help for.
Imagine we have two sets of data, where one contains proper data, i.e.:
var data = {
data1 : 0
};
The other a collection of objects describing 3 elements :
var elements = {
1 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
2 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
3 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
}
}
See the complete codesnippet below
var data = {
data1 : 0
};
var elements = {
1 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
2 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
3 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
}
}
// This is our main object, we define the properties only ...
var _elem = function (props,id){
this.id = id;
this.target = document.getElementById(props.target);
this.element = document.createElement(props.element);
this.events = props.events;
this.value = props.value;
}
// Then we add a method to render it on the page ...
_elem.prototype.render = function(){
// I added the Object Id for debugging purposes
this.element.innerHTML = this.value.data1 + ' ['+this.id+']';
this.target.appendChild(this.element);
}
// ... and another to change the underlying data and re - render it
_elem.prototype.add = function(){
// Since the data is a reference to the same data object
// We expect to change the value for all the elements
this.value.data1++;
this.render();
}
// First we looop trough the array with the element definition and
// Cast each item into a new element
for(var el in elements){
elements[el] = new _elem(elements[el],el);
}
// Then we apply the event listener (if any event description is present)
for(var el in elements){
if(!elements[el].hasOwnProperty( 'events' )){
continue;
}
// We use the anonymous function here to avoid the "common mistake"
(function() {
var obj = elements[el];
var events = obj.events;
for(var ev in events){
obj.element.addEventListener(ev,function(){ obj[events[ev]]() });
}
})();
}
// And finally we render all the elements on the page
for(var el in elements){
elements[el].render(elements[el]);
}
div {
padding: 10px;
border: solid 1px black;
margin: 5px;
display: inline-block;
}
<html>
<head>
</head>
<body>
<div id="main"></div>
</body>
</html>
Now, if we click button [1] it will update itself and the following, resulting in this sequence:
0 [2] 0 [3] 1 [1]
We refresh the page and this time click button [2], the sequence will be:
0 [1] 0 [3] 1 [2]
Button [3] Instead will update itself only
0 [1] 0 [2] 1 [3]
I did look for this topic before posting but all I could find were questions similar to this: addEventListener using for loop and passing values , where the issue was the counter variable holding always the last value
In this case instead it seems the issue to be the opposite, or the object holding the initial value and the ones following (if you keep clicking you will see what I mean)
What am I do wrong?
The issue appears to be that you are re-appending your child elements on each "refresh", which shifts the order of the elements and gives the illusion of refreshing multiple elements.
You need to differentiate between an initial render and subsequent refreshes.
I recommend that you remove the append from your render function and instead handle appending in your final for loop:
// And finally we render all the elements on the page
for(el in elements){
elements[el].render(elements[el]);
elements[el].target.append(elements[el].element);
}
Note that there are multiple "issues" with your code, including global variables in several locations. And I'm not confident that your architecture will scale well. But, those issues are outside the scope of your question, and you'll learn as you go... no reason to expect that everyone will know everything, and you may find that your current solution works just fine for what you need it to do.
var data = {
data1 : 0
};
var elements = {
1 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
2 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
},
3 : {
target : 'main',
value : data,
element : 'div',
events : {
click : 'add'
}
}
}
// This is our main object, we define the properties only ...
var _elem = function (props,id){
this.id = id;
this.target = document.getElementById(props.target);
this.element = document.createElement(props.element);
this.events = props.events;
this.value = props.value;
}
// Then we add a method to render it on the page ...
_elem.prototype.render = function(){
// I added the Object Id for debugging purposes
this.element.innerHTML = this.value.data1 + ' ['+this.id+']';
}
// ... and another to change the underlying data and re - render it
_elem.prototype.add = function(){
// Since the data is a reference to the same data object
// We expect to change the value for all the elements
this.value.data1++;
this.render();
}
// First we looop trough the array with the element definition and
// Cast each item into a new element
for(var el in elements){
elements[el] = new _elem(elements[el],el);
}
// Then we apply the event listener (if any event description is present)
for(var el in elements){
if(!elements[el].hasOwnProperty( 'events' )){
continue;
}
// We use the anonymous function here to avoid the "common mistake"
(function() {
var obj = elements[el];
var events = obj.events;
for(ev in events){
obj.element.addEventListener(ev,function(){ obj[events[ev]]() });
}
})();
}
// And finally we render all the elements on the page
for(var el in elements){
elements[el].render(elements[el]);
elements[el].target.appendChild(elements[el].element);
}
div {
padding: 10px;
border: solid 1px black;
margin: 5px;
display: inline-block;
}
<html>
<head>
</head>
<body>
<div id="main"></div>
</body>
</html>

changing backbone views

I have a question about the way backbone handles it views.
Suppose I have the following code:
<div id="container">
<div id="header">
</div>
</div>
After this I change header into a backbone view.
How can I now remove that view from the header div again after I'm done with the view and add ANOTHER view to the same div?
I tried just overwriting the variable the view was stored in. This results in the view being changed to the new one...but it will have all the event handlers of the old one still attached to it.
Thanks in advance!
http://documentcloud.github.com/backbone/#View-setElement
This won't automatically remove the original div - you'll want to do that yourself somehow, but then by using setElement you'll have the view's element set to whatever you passed it.. and all of the events will be attached as appropriate. Then you'll need to append that element wherever it is that it needs to go.
--- Let's try this again ----
So, first thing to keep in mind is that views reference DOM elements.. they aren't super tightly bound. So, you can work directly with the jquery object under $el.
var containerView = new ContainerView();
var headerView = new HeaderView();
var anotherHeaderView = new AnotherHeaderView();
containerView.$el.append(headerView.$el);
containerView.$el.append(anotherHeaderView.$el);
anotherHeaderView.$el.detach();
containerView.$el.prepend(anotherHeaderView.$el);
Or you can create methods to control this for you.
var ContainerView = Backbone.View.extend({
addView: function (view) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.$el.append(el);
}
});
Maybe setting the views by view order?
var ContainerView = Backbone.View.extend({
initialize: function () {
this.types = {};
},
addView: function (view, type) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.types[type] = el;
this.resetViews();
},
removeView: function (type) {
delete this.types[type];
this.resetViews();
},
resetViews: function () {
this.$el.children().detach();
_.each(['main_header', 'sub_header', 'sub_sub_header'], function (typekey) {
if(this.types[typekey]) {
this.$el.append(this.types[typekey]);
}
}, this);
}
});

Limit number of characters in View (Backbone.js)

I have a backbone.js View that reads a template from the HTML file and inserts values from its model into the template. One of this value is in the variable title, which can be long enough to disrupt the flow of elements on the page. I want to use Javascript to limit the max. number of characters title can have, instead of doing it on the backend because eventually the full title has to be displayed.
I tried selecting the div that contains title after the template was been rendered, but cannot seem to select it. How can I do this otherwise?
Template
<script type="text/template" id="tpl_PhotoListItemView">
<div class="photo_stats_title"><%= title %></div>
</script>
View
PhotoListItemView = Backbone.View.extend({
tagNAme: 'div',
className: 'photo_box',
template: _.template( $('#tpl_PhotoListItemView').html() ),
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
console.log($(this.el).children('.photo_stats_title')); <!-- returns nothing -->
this.limitChars();
return this;
},
limitChars: function() {
var shortTitle = $(this.el).children('.photo_stats_title').html().substring(0, 10);
$(this.el .photo_stats_title).html(shortTitle);
}
});
Rather than try to modify the title after rendering it, modify it as it's rendered.
Pass a maxlength variable to your template as well, then:
<script type="text/template" id="tpl_PhotoListItemView">
<div class="photo_stats_title"><%= title.substr(0,maxLength) %></div>
</script>
If title.length is less than maxlength, the entire string will display. If it's greater, only the first maxlength characters will display.
Alternatively, simply hardcode the maximum length of the title into the call to .substr()
If you need to perform more advanced truncating (e.g. adding '...' to truncated titles), you're better off modifying the title before rendering the template, passing the truncated version of the title into the template
Another option would be to override Model.parse(response), creating a shortTitle attribute on the model; this way it's always available whenever you're working with the model
Two things, the first one, to get any View's children I recommend you this way instead of what you are doing:
console.log( this.$('.photo_stats_title') );
"this.$" is a jQuery selector with the specific scope of your view.
The second thing is to wrap your model to handle this, I do not suggest to validate this in your Template or your View. In your Model define a new attribute for the shortTitle:
var titleMaxLength = 20;
var YourModel : Backbone.Model.extend({
defaults : {
id : null,
shortTitle : null,
title : null
},
initialize : function(){
_.bindAll(this);
this.on('change:name', this.changeHandler);
this.changeHandler();
},
changeHandler : function(){
var shortTitle = null;
if( this.title ){
shortTitle = this.title.substr(0, titleMaxLength);
}
this.set({ shortTitle : shortTitle }, {silent: true});
}
});

Nested Templates in JavaScript/jQuery/AJAX

I am trying to accomplish a module where i need to use nested templates and I am stuck at HOW can i do that.
basically there are 3 levels in my UI, for example say Level 1, Level 2, Level 3.
So when the page is displayed i need to render level 1 only.
But when user clicks on "expand" button of any element of level1 i need to render corresponding elements of Level 2 (not all) below the selected element of level 1.
Now when user clicks on "expand" of any element of Level 2, corresponding Level 3 should be rendered..
To summarize it should be just like Windows Explorer's navigation bar on left.!
Generally, you should define separate components for each level, assign template to each of your components and implement something like expand()/collapse() methods. If a component is initially collapsed (your case) then it shouldn't need to render child items on initialization, it would render them only when you expand them (the appropriate templates of child components would be used).
Please provide a basic code that you are trying to make work, it would be easier to help you that way.
Here is a quick prototype of Widget system with a simple rendering flow that uses templates. I guess you want something like that in your application. It is unoptimized, it's just an idea of how your framework might look.
/**
* Widget constructor
*/
var Widget = function(config) {
// apply config
$.extend(this, config);
// attempt to render
this.render();
};
/**
* Widget prototype
*/
$.extend(Widget.prototype, {
// render target
renderTo: null,
// template
tpl: '<div class="container-panel">' +
'<p>${txt}</p>' +
'<div class="items-container"></div>' +
'</div>',
// template data
tplData: null,
// child items array
children: null,
// initial collapsed state
collapsed: false,
// widget's root element
el: null,
// default render target selector for child items
renderTarget: '.items-container',
render: function() {
var me = this,
renderDom
// render the widget
if(!this.rendered && this.renderTo && this.tpl) {
renderDom = $.tmpl(this.tpl, this.tplData);
// assume that first element is widget's root element
this.el = renderDom[0];
$(this.renderTo).append(renderDom);
// clear the reference
renderDom = undefined;
// THIS IS JUST EXAMPLE CODE! Bind click handler...
$(this.el).find('p').first().click(function() {
me.collapsed ? me.expand() : me.collapse();
});
// find render target for children
this.renderTarget = $(this.el).find(this.renderTarget).first();
// render children if not collapsed
this.renderChildren();
// set rendered flag
this.rendered = true;
}
},
renderChildren: function() {
var children = this.children;
if(!this.collapsed && children && children.length) {
for(var i = 0, len = children.length; i < len; i++) {
// render children inside
children[i].renderTo = this.renderTarget;
children[i].render();
}
}
},
/**
* Expand template method. Override it.
*/
expand: function() {
this.collapsed = false;
this.renderChildren();
this.renderTarget.show();
},
/**
* Collapse template method. Override it.
*/
collapse: function() {
this.collapsed = true;
this.renderTarget.hide();
}
});
​Here I pre-defined the templates and hardcoded the expanding/collapsing logic that happens on click inside widget's first paragraph element.
This is how you would use the widgets:
// Using our widgets
var containerPanel = new Widget({
tplData: {txt: 'Hello world!'},
renderTo: $('body'),
collapsed: true,
children: [
new Widget({
tplData: {txt: ' Child 1'},
collapsed: true,
children: [
new Widget({
tplData: {txt: ' Child 1.1'}
}),
new Widget({
tplData: {txt: ' Child 1.2'}
}),
new Widget({
tplData: {txt: ' Child 1.3'}
})
]
}),
new Widget({
tplData: {txt: ' Child 2'}
})
]
});
You can see a live example on jsFiddle: http://jsfiddle.net/dipish/XDmWq/
Just click on items and look at the dynamically generated markup.
I think the code is self-explanatory but feel free to ask any questions. Note that the code uses jQuery Templates Plugin but it is just for convenience.
If you have many complex components in your web app you may want to use something more serious than bare jQuery, like ExtJS or Dojo Toolkit. Such frameworks typically provide you a convenient class system and base widget/component logic to build on, besides lots of other things.
Good luck!
You'll need to be a bit more specific in what the markup will look like. However, here's a rough example:
//run this puppy when we need to append stuff
var dynamicAppend = function( data, container )
{
var ul = container.children().slice(1,2);
var len = data.length;
for( var i = 0; i < len; i++ )
{
var markup = [
"<li class='" + data[i].thing + "'>",
"<span class='second_toggle' data-stuff='" + data[i].other_thing + "'></span>",
"<ul>",
"</ul>",
"</li>"
];
ul.append( markup.join() );
}
}
//do ajax stuff
var handleAjax = function( data, container )
{
var json = { unique: data }
$.ajax({
url: '',
data: json,
success: function( data )
{
if( data.success === 'your_flag' && data.newStuff )
{
dynamicAppend( data.newStuff, container );
}
}
});
}
//first, you'll click the toggle
var expand_toggle = $( '.toggle' );
expand_toggle.click(function(e){
var that = $(this);
//grab some data that identifies the unique container you want to append to
var unique_id = that.data( 'some_identifier' );
var container = that.parents('.parent_container_class:first');
handleAjax( unique_id, container );
});
I would personally put this into a constructor and do it OOP style, but you can get the idea.
Here's some markup:
<div class='parent_container_class'>
<span class='toggle' data-some_identifier='special_identifier_here'></span>
<ul></ul>
</div>

Categories