Backbone.js: avoid view→model→view double conversion - model-view-controller

I’m trying to build this:
When I edit field on the left it should update the one on the right and vice-versa.
Editing a value in an input field causes the text cursor to jump at the end of it.
Typing "2" in the fahrenheit field gets replaced with 1.999999999999, as you can see on the screenshot. This happens because of the double conversion:
view’s Fº → model’s Cº → view’s Fº.
How can I avoid that?
Update:
I want to know the elegant way of dealing with two-way bindings in MVC frameworks such as Backbone.js.
MVC
var Temperature = Backbone.Model.extend({
defaults: {
celsius: 0
},
fahrenheit: function(value) {
if (typeof value == 'undefined') {
return this.c2f(this.get('celsius'));
}
value = parseFloat(value);
this.set('celsius', this.f2c(value));
},
c2f: function(c) {
return 9/5 * c + 32;
},
f2c: function(f) {
return 5/9 * (f - 32);
}
});
var TemperatureView = Backbone.View.extend({
el: document.body,
model: new Temperature(),
events: {
"input #celsius": "updateCelsius",
"input #fahrenheit": "updateFahrenheit"
},
initialize: function() {
this.listenTo(this.model, 'change:celsius', this.render);
this.render();
},
render: function() {
this.$('#celsius').val(this.model.get('celsius'));
this.$('#fahrenheit').val(this.model.fahrenheit());
},
updateCelsius: function(event) {
this.model.set('celsius', event.target.value);
},
updateFahrenheit: function(event) {
this.model.fahrenheit(event.target.value);
}
});
var temperatureView = new TemperatureView();
No MVC
celsius.oninput = function(e) {
fahrenheit.value = c2f(e.target.value)
}
fahrenheit.oninput = function(e) {
celsius.value = f2c(e.target.value)
}
function c2f(c) {
return 9/5 * parseFloat(c) + 32;
}
function f2c(f) {
return 5/9 * (f - 32);
}
Not only it fixes the problem, it’s also reduces the code 3.5⨉. Clearly I’m doing MVC wrong.

Here's my take on this; instead rendering the whole view on every change, in interactive views, use the view's jQuery or plain JS contexts just like your non-MVC example.
http://jsbin.com/fomugixe/1/edit
As the Backbone docs say:
"Two way data-binding" is avoided. While it certainly makes for a nifty demo, and works for the most basic CRUD, it doesn't tend to be terribly useful in your real-world app. Sometimes you want to update on every keypress, sometimes on blur, sometimes when the panel is closed, and sometimes when the "save" button is clicked.

Two methods come to mind. As Kinakuta mentioned you can do something like the following so you're math works on integers, instead of decimals:
temp = ((oldTemp * 100) * conversion stuff) / 100
Depending on how complex you want your app to be you can also use something like Backbone.ModelBinder. It automatically binds your view to your model so when one updates, the other updates automatically. You can then attach a converter function to the binding so when your value goes view -> model or model -> view it's run through the converter. I can elaborate more if that idea interests you.
Update: With a simple temp converter it's not surprising that Backbone requires 3.5x as much code. An MVC framework can reduce bloat in a large project, but for a small app it might be overkill. e.g. imagine using Backbone to display "Hello World".
As for your issue, how about only rendering the other input value when one is changed, instead of both? If F input changes, re-render value in C box. With ModelBinder I would do this by having two attributes in my model: tempF and tempC. When one is modified, I re-calculate the other and ModelBinder automatically displays it. Or you can go without MB and just listen for the change event.

set a variable at the view level where you hold the input field that started the conversion, so you don't call the conversion function on that field.

Related

SAPUI5: Extend Control, renderer has html tags with event

I extend a Control to create a new custom control in UI5 and this control renders a tree as UL items nicely. Now I need to implement a collapse/expand within that tree. Hence my renderer writes a tag like
<a class="json-toggle" onclick="_ontoggle"></a>
and within that _ontoggle function I will handle the collapse/expand logic.
No matter where I place the _ontoggle function in the control, I get the error "Uncaught ReferenceError: _ontoggle is not defined"
I am missing something obvious but I can't find what it is.
At the moment I have placed a function inside the
return Control.extend("mycontrol",
{_onToggle: function(event) {},
...
Please note that this event is not one the control should expose as new event. It is purely for the internals of how the control reacts to a click event.
I read things about bind and the such but nothing that made sense for this use case.
Took me a few days to crack that, hence would like to provide you with a few pointers.
There are obviously many ways to do that, but I wanted to make that as standard as possible.
The best suggestion I found was to use the ui5 Dialog control as sample. It consists of internal buttons and hence is similar to my requirement: Render something that does something on click.
https://github.com/SAP/openui5/blob/master/src/sap.ui.commons/src/sap/ui/commons/Dialog.js
In short, the solution is
1) The
<a class="json-toggle" href></a>
should not have an onclick. Neither in the tag nor by adding such via jQuery.
2) The control's javascript code should look like:
sap.ui.define(
[ 'sap/ui/core/Control' ],
function(Control) {
var control = Control.extend(
"com.controlname",
{
metadata : {
...
},
renderer : function(oRm, oControl) {
...
},
init : function() {
var libraryPath = jQuery.sap.getModulePath("mylib");
jQuery.sap.includeStyleSheet(libraryPath + "/MyControl.css");
},
onAfterRendering : function(arguments) {
if (sap.ui.core.Control.prototype.onAfterRendering) {
sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments);
}
},
});
control.prototype.onclick = function (oEvent) {
var target = oEvent.target;
return false;
};
return control;
});
Nothing in the init(), nothing in the onAfterRendering(), renderer() outputs the html. So far there is nothing special.
The only thing related with the onClick is the control.prototype.onclick. The variable "target" is the html tag that was clicked.

How to use SuperScrolloRama with Backbone.js Views

I am trying to understand how to use a plugin like http://johnpolacek.github.io/superscrollorama/, with Backbone.js by integrating it into my Views. I know that I need to hook into the backbone View-Events, but I want to do a horizontal scroll with the plugin, and I don't know of a horizontal scroll-event. How can I still utilize the plugin? Thanks in advance for any ideas.
Views:
var ArtistsView = Backbone.View.extend({
tagName: 'ul',
initialize: function () {
this.cleanUp();
$("body").attr('id','artists');
this.render();
},
events: {
"click div.open" : "largeArtViewOpen",
"click div.close" : "largeArtViewClose",
},
render: function () {
this.collection.each(function(model) {
var artistView = new ArtistView({ model: model });
this.$el.append(artistView.render().el);
}, this);
console.log('and a new view was rendered!')
return this;
},
cleanUp: function(){
if (this != null) {
this.remove();
this.unbind();
console.log('View was removed!');
}
},
largeArtViewOpen: function(e){
var thisArt = $(e.currentTarget).parent().attr("class");
console.log(thisArt);
$("#open-view, li."+thisArt).show();
},
largeArtViewClose: function(e){
//var thisArt = $(e.currentTarget).parent().attr("class");
console.log('clicked!');
$("#open-view, ul#large li").hide();
},
scrollFx: function(){
var controller = $.superscrollorama({
isVertical:false
});
controller.addTween('h2#fade-it', TweenMax.from( $('h2#fade-it'), .5, {css:{opacity: 0}}), 800);
//$('h2#fade-it').css({'color':'#dbdbdb'});
console.log('scroll message!');
},
});
var ArtistView = Backbone.View.extend({
tagName:'li',
className:'artistLink not-active',
render: function(){
this.id = this.model.get('idWord')+"-menu-item";
this.$el.attr('id', this.id).html(this.template(this.model.toJSON()));
return this;
},
});
So, in the past 3 days since I've asked this question, I've spent some time trying different scrollable 'targets' for Superscrollorama...Document vs. Window vs. Body vs. other DOM elements within the HTML, and the questions that I've had to consider are, should the scroll event be bound to the View's top element? Should it be bound to the body, but initialized in the view? In both cases I tried, I couldn't get the scroll events to continuously fire...this may just be due to bad code, but I couldn't make it happen.
So, what I arrived at, was, avoiding the view entirely: I instantiating and called Superscrollorama in a function called scrollFx() within a separate 'helper.js' document, and then called scrollFx() from my view's router.
I'm thinking I will just empty the target's styles and unbind any existing scroll events in the beginning of scrollFx(), before I call the Superscrollorama function so that the resulting scroll styles/animations are cleaned up, and events aren't exponentially bound.
I'm still very much working through these issues, though now the scroll events are working, so if anyone happens to read through this train of thought, please feel free to add your two sense, especially, if you have better ideas about re-implementing the Superscrollorama function within the View itself.
Thanks.

Kendo UI grid - batch update not executed

I'm implementing a simple (at least ,that was the goal) Kendo UI grid that displays two columns: one holding a checkbox, bound to a boolean, and one holding a display name for the item. The checkbox column has a simple template, and the change() event of the checkbox is handled so that the model in the datasource gets updated. I have verified this, and it works.
The data source has been configured for batch, and defines a transport for read and update. Both call a function that perform the ajax call. As I said before, the read function is handled as expected. However, the update function defined on the transport is not. The sync() on the datasource is triggered with a simple button whose click event is hooked to a function that calls datasource.sync() (or grid.saveChanges()).
transport: {
read: function(options) {
return loadStuff(options);
},
update: function (options) {
return updateStuff(options);
}
}
When debugging in the Kendo UI code, it looks like the models attribute on the ModelSet is always empty, and therefore the sync() decides that there's nothing to sync. Anyone got a clue what is happening here?
UPDATE:
Looks like something may be wrong when handling the checkbox check / uncheck. Apparently I should use something like
$('#divGrid').on('click', '.chkbx', function() {
var checked = $(this).is(':checked');
var grid = $('#divGrid').data().kendoGrid;
var dataItem = grid.dataItem($(this).closest('tr'));
dataItem.set("Selected", checked);
});
Unfortunately, it looks like the set() method is not defined on the data item. When debugging, it only contains the data, and no Model object having the set() method.
UPDATE 2:
Tried wrapping the data returned from the ajax call in a model defined with Model.define(). That seems to solve the issue of the model not being dirty, as the _modified property on the model returns true. However, the models array in the ModelSet remains empty. Is this a bug in Kendo UI, or am I going the wrong way?
You don't actually need to bind to click event on the checkboxes.
I´ve posted an example on using it in JSFiddle where you can see it running. This example displays in a grid two columns: first text (tick) and second boolean rendered as a checkbox (selected); the update is batch (so, it's pretty close to what you have).
Questions to keep in mind are:
For displaying the checkbox while not in edit mode, you should define a template, something like this. You might realize that the checkbox is in disabled state by default since you want to edit it as other fields (selecting the cell first). This also guarantees that the model is correctly updated:
{
field : "selected",
title : "Selected",
template: "<input type='checkbox' name='selected' #= selected ? 'checked' : '' # disabled/>"
}
Define in the model that this field is boolean:
schema : {
id : "id",
model: {
fields: {
symbol : { type: "string" },
selected: { type: "boolean" }
}
}
},
Define the transport.update function, something like:
transport: {
read : function (operation) {
// Your function for reading
},
update: function (operation) {
// Display modified data in an alert
alert("update" + JSON.stringify(operation.data.models, null, 4));
// Invoke updating function
// that should ends with an operation.success(the_new_data)
// In this example just say ok
operation.success(operation.data.models)
}
}
EDIT: If you want to be able to modify the checkbox state without having to enter in edit mode first, you should:
Remove the disabled from the template:
{
field : "selected",
title : "Selected",
template : "<input type='checkbox' name='selected' #= selected ? 'checked' : '' #/>"
},
Then bind the click event on checkboxes to the following handler function:
$("#stocks_tbl").on("click", "input:checkbox", function(ev) {
var dataItem = grid.dataItem($(this).closest('tr'));
dataItem.set("selected", this.checked);
});
Where #stocks_tbl is the id of the div that contains the grid. You might see it running here.
NOTE: It's important the on with the three parameters for making it live

Titanium Mobile: reference UI elements with an ID?

How do you keep track of your UI elements in Titanium? Say you have a window with a TableView that has some Switches (on/off) in it and you'd like to reference the changed switch onchange with a generic event listener. There's the property event.source, but you still don't really know what field of a form was just toggled, you just have a reference to the element. Is there a way to give the element an ID, as you would with a radiobutton in JavaScript?
Up to now, registered each form UI element in a dictionary, and saved all the values at once, looping through the dictionary and getting each object value. But now I'd like to do this onchange, and I can't find any other way to do it than create a specific callback function for each element (which I'd really rather not).
just assign and id to the element... all of these other solution CAN work, but they seem to be over kill for what you are asking for.
// create switch with id
var switcher0 = Ti.Ui.createSwitch({id:"switch1"});
then inside your event listener
myform.addEventListener('click', function(e){
var obj = e.source;
if ( obj.id == "switch1" ) {
// do some magic!!
}
});
A simple solution is to use a framework that helps you keep track of all your elements, which speeds up development quite a bit, as the project and app grows. I've built a framework of my own called Adamantium.js, which lets you use a syntax like jQuery to deal with your elements, based on ID and type selectors. In a coming release, it will also support for something like classes, that can be arbitrarily added or removed from an element, tracking of master/slave relationships and basic filter methods, to help you narrow your query. Most methods are chainable, so building apps with rich interaction is quick and simple.
A quick demo:
// Type selector, selects all switches
$(':Switch')
// Bind a callback to the change event on all switches
// This callback is also inherited by all new switch elements
$(':Switch').bind('change', function (e) {
alert(e.type + ' fired on ' + e.source.id + ', value = ' + e.value);
});
// Select by ID and trigger an event
$('#MyCustomSwitch').trigger('change', {
foo: 'bar'
});
Then there's a lot of other cool methods in the framework, that are all designed to speed up development and modeled after the familiar ways of jQuery, more about that in the original blog post.
I completely understand not wanting to write a listener to each one because that is very time consuming. I had the same problem that you did and solved it like so.
var switches = [];
function createSwitch(i) {
switches[i] = Ti.UI.createSwitch();
switches[i].addEventListener('change', function(e) {
Ti.API.info('switch '+i+' = '+e.value);
});
return switches[i];
}
for(i=0;i<rows.length;i++) {
row = Ti.UI.createTableViewRow();
row.add(createSwitch(i));
}
However keep in mind that this solution may not fit your needs as it did mine. For me it was good because each time I created a switch it added a listener to it dynamically then I could simply get the e.source.parent of the switch to interact with whatever I needed.
module Id just for the hold it's ID. When we have use id the call any another space just use . and use easily.
Try This
var but1 = Ti.Ui.createButton({title : 'Button', id:"1"});
window.addEventListener('click', function(e){
var obj = e.source;
if ( obj.id == "1" ) {
// do some magic!!
}
});
window.add(but1);
I, think this is supported for you.
how do you create your tableview and your switcher? usually i would define a eventListener function while creating the switcher.
// first switch
var switcher0 = Ti.Ui.createSwitch();
switch0.addEventListener('change',function(e){});
myTableViewRow.add(switch0);
myTableView.add(myTableViewRow);
// second switch
var switch1 = ..
so no generic event listener is needed.

prototype get inner html of multiple classes

I am new to prototype and finding it a lot more difficult than jquery. All i want to do is get the inner html of various classes.
$$('.book').each(function() {
var msg = this.down(".information");
alert(msg.innerHTML);
//new Tip(this, msg.innerHTML, {stem: 'topLeft', hook: { tip: 'topLeft', mouse: true }, offset: { x: 14, y: 14 }});
});
I'm trying to create tooltips for multiple items, but I'm not even getting the alert.
I think you can probably prevent the extra dom work of down() like this:
$$('.book .information').each(function(book) {
alert(book.innerHTML);
});
remember you also have the ability to use advanced CSS2 and CSS3 selectors in prototype like this for example:
$$('.book a[rel]').each(function(el) {
alert(el.rel);
});
see the bottom of this page for more examples http://www.prototypejs.org/api/utility/dollar-dollar
The this variable is not pointing to the element you're iterating over in Prototype, you have to explicitly use a parameter:
$$('.book').each(function(book) {
var msg = book.down(".information");
alert(msg.innerHTML);
});

Resources