So I've been struggling for quire sometime to do this. I am building a dynamic form generator tool. One of the functions is that the user should be able to use a jQuery slider to select the font size. I use Underscore templates to create divs for the slider. So everytime a user selects a Label input, this underscore template is loaded and I call the slider() fn to initialize it. Here's is the part of the code that does this
UnderscoreTemplate
<!-- template for plain text inpt to be shown in form generate -->
<script type="text/template" id="text-generate-template">
<div class="row">
<div class="col-sm-10">
<input type="text" placeholder="Enter text here" class="label-name form-control" value="<%=model.get('label')%>">
</div>
<button type="button" class="close" id="remove-element" >×</button>
</div>
<div class="row col-sm-6" style="margin-top:10px">
<div id="slider-<%=model.cid%>"></div>
<small>Font Size: <%=model.get('fontSize')%>px </small>
</div>
</script>
So as per the code ablove, each dynamically loaded element has a unique slider with a unique ID.
The Backbone Model extends from a Base element called "Element"
var TextElement = Element.extend({
defaults:function(){
return _.extend({}, Element.prototype.defaults,{
name: 'PlainText',
generateTemplateType: 'text-generate-template',
previewTemplateType:'text-template',
textAlign: 'center'
});
}
});
The view which is responsible for generating the html is
var ElementView = Backbone.View.extend({
tagName: 'li',
events : {
},
initialize : function(){
this.listenTo(this.model,'change', this.render);
this.render();
},
render : function(){
var htmlContent = $('#'+this.model.get('generateTemplateType')).html();
var content = _.template( htmlContent, {elementType : elementTypes, model : this.model} );
this.$el.html( content );
me = this;
if(this.model.get('name') == 'PlainText'){
this.$el.find('#slider-'+this.model.cid).slider({
min: 10,
max:40,
step: 1,
value:me.model.get('fontSize') > 9 ? me.model.get('fontSize') : 24,
change: function(event, ui) {
me.slide(event, ui.value);
}
});
}
return this;
},
slide: function(event, index){
console.log("the models id in slide function -> "+this.model.cid);
this.model.set('fontSize', index);
}
});
So what I do here is that every time I detect a change, I update this model's fontSize to the new value passed in through the Slider.
THe collection to which this model belongs to recognises this changes and rerenders itself.
When there is only one such element on the page, then everything works fine. The moment the user adds more than one element and tried to change the font size for each item, then only the font size of the last element is changed. Regardless of whether I slide the first , second or the third element it always sets the change to the last model in the collection and the last model get rerendered with the incorrect font size value.
I tried console logging the id of the model in the slide function and it always seems to return the last models ID regardless of which slider I use.
What am I doing wrong here??
You have an accidental global variable right here:
me = this;
A side effect of this is that every instance of ElementView will end up sharing exactly the same me and that me will be the last ElementView you instantiate. Sound familiar?
The solution is to use a local variable:
var me = this;
or, if you don't need the slider's this inside the callback, use a bound function instead:
value: this.model.get('fontSize') > 9 ? this.model.get('fontSize') : 24,
change: _(function(event, ui) {
this.slide(event, ui.value);
}).bind(this)
You could also use $.proxy or Function.prototype.bind if you prefer those over _.bind.
Related
I apologize, but I'm not able to provide a working jsFiddle snippet. I will update the question if I understand how to put the code below in it.
Using dojox/mobile I populate an EdgeToEdgeStoreList with custom ListItems. Some code:
html (jade)
div(data-dojo-type="dojox/mobile/View")
h1(data-dojo-type="dojox/mobile/Heading") Device List
div(data-dojo-type="dojox/mobile/ScrollablePane")
ul#list(data-dojo-type="dojox/mobile/EdgeToEdgeStoreList" data-dojo-props="itemRenderer: DeviceListItem, select: 'single'")
js
var store;
var list = registry.byId("listDevices");
var devices = JSON.parse("a string received from server");
store = new Memory({data: devices, idProperty: "label"});
list.setStore(store);
DeviceListItem
define([
"dojox/mobile/ListItem",
"dijit/_TemplatedMixin",
"dojo/_base/declare"
], function (ListItem, TemplatedMixin, declare) {
var template =
"<div class='deviceDone${done}'>" +
" ${id} - <div style='display: inline-block;' data-dojo-attach-point='labelNode'></div>" +
" <div class='deviceCategory'>${category}</div>" +
"</div>";
TemplatedListItem = declare("DeviceListItem",
[ListItem, TemplatedMixin], {
id: "",
label: "",
category: "",
done: "false",
templateString: template
}
);
});
It works fine, that is I will see my custom ListItems.
But if I resize the window (on desktop browsers) or change orientation (on mobile ones) only the ${id} field remains visible. The others (label and category) disappear. The behavior is the same in all browsers (that I tried).
After debugging I discovered the following. Before any resize the actual html of a ListItem looks like this:
<div id="item1728" class="deviceDoneFalse mblListItem mblListItemUnchecked" tabindex="0" widgetid="item1728" aria-selected="false" role="option">
item1728 -
<div style="display: inline-block;" data-dojo-attach-point="labelNode">n.a.</div>
<div class="deviceCategory">General purpose</div>
</div>
and it's like the template string. After a resize the inner div becomes:
<div style="display: block;" data-dojo-attach-point="labelNode">n.a.</div>
without "inline" all the layout will mess-up and thus the fields "disappear" (actually go below, behind the next row).
I wonder why this happens - the display style is hardcoded into the template strings!
Furthermore, I inspected the CSS rules at runtime, and it's not due to them, it's the html that has changed - indeed.
ListItem (source in dojox/Mobile/ListItem.js) has the following function:
resize: function(){
if(this.variableHeight){
this.layoutVariableHeight();
}
// labelNode may not exist only when using a template (if not created by an attach point)
if(!this._templated || this.labelNode){
// If labelNode is empty, shrink it so as not to prevent user clicks.
this.labelNode.style.display = this.labelNode.firstChild ? "block" : "inline";
}
},
This function is called after a resize and as you can see sets the labelNode display style to "block".
You can replace this function when you define your DeviceListItem, keeping the original source as is but changing the display style.
I've been using an implementation of this Drag and Drop with AngularJS and jQuery UI:
http://www.smartjava.org/examples/dnd/double.html
With AngularJS 1.0.8 it works flawlessly. With 1.2.11, it doesn't.
When using AngularJS 1.2 and dragging an item from the left list to the right one the model for the destination list updates correctly. However the DOM doesn't update correctly. Here is the directive that's being used from the example:
app.directive('dndBetweenList', function($parse) {
return function(scope, element, attrs) {
// contains the args for this component
var args = attrs.dndBetweenList.split(',');
// contains the args for the target
var targetArgs = $('#'+args[1]).attr('dnd-between-list').split(',');
// variables used for dnd
var toUpdate;
var target;
var startIndex = -1;
// watch the model, so we always know what element
// is at a specific position
scope.$watch(args[0], function(value) {
toUpdate = value;
},true);
// also watch for changes in the target list
scope.$watch(targetArgs[0], function(value) {
target = value;
},true);
// use jquery to make the element sortable (dnd). This is called
// when the element is rendered
$(element[0]).sortable({
items:'li',
start:function (event, ui) {
// on start we define where the item is dragged from
startIndex = ($(ui.item).index());
},
stop:function (event, ui) {
var newParent = ui.item[0].parentNode.id;
// on stop we determine the new index of the
// item and store it there
var newIndex = ($(ui.item).index());
var toMove = toUpdate[startIndex];
// we need to remove him from the configured model
toUpdate.splice(startIndex,1);
if (newParent == args[1]) {
// and add it to the linked list
target.splice(newIndex,0,toMove);
} else {
toUpdate.splice(newIndex,0,toMove);
}
// we move items in the array, if we want
// to trigger an update in angular use $apply()
// since we're outside angulars lifecycle
scope.$apply(targetArgs[0]);
scope.$apply(args[0]);
},
connectWith:'#'+args[1]
})
}
});
Does something need to be updated for this to work properly with Angular 1.2? I feel like it has something to do with the scope.$apply but am not sure.
I see this is an older question, but I recently ran into the exact same issue with the Drag and Drop example. I don’t know what has changed between angular 1.0.8 and 1.2, but it appears to be the digest cycle that causes problems with the DOM. scope.$apply will trigger a digest cycle, but scope.$apply in and of itself is not the issue. Anything that causes a cycle can cause the DOM t get out of sync with the model.
I was able to find a solution to the the problem using the ui.sortable directive. The specific branch that I used is here: https://github.com/angular-ui/ui-sortable/tree/angular1.2. I have not tested with other branches.
You can view a working example here:
http://plnkr.co/edit/atoDX2TqZT654dEicqeS?p=preview
Using the ui-sortable solution, the ‘dndBetweenList’ directive gets replaced with the ui-sortable directive. Then there are a few changes to make.
In the HTML
<div class="row">
<div class="span4 offset2">
<ul ui-sortable="sortableOptions" ng-model="source" id="sourceList" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-danger nomargin" ng-repeat="item in source">{{item.value}}</li>
</ul>
</div>
<div class="span4">
<ul ui-sortable="sortableOptions" id="targetList" ng-model="model" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-info nomargin" ng-repeat="item in model">{{item.value}}</li>
</ul>
</div>
</div>
Note the dnd-between-list directive is no longer needed and is replaced with the ui-sortable.
In the module inject the ui-sortable, and in the controller specify that sortable options. The sortable accepts the same options as the jquery sortable.
app.js
var app = angular.module('dnd', ['ui.sortable']);
ctrl-dnd.js
$scope.sortableOptions = {
connectWith: '.connector'
}
Only the additions to the controller are shown. Note that I added a .connector class on the ul. In the sortable I use .connector for the connectWith option.
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() }
},
});
In a basic table structure, I want to be able to display a set of data from an array of objects one at a time. Clicking on a button or something similar would display the next object in the array.
The trick is, I don't want to use the visible tag and just hide the extra data.
simply you can just specify property that indicate the current element you want to display and index of that element inside your observableArray .. i have made simple demo check it out.
<div id="persons"> <span data-bind="text: selectedPerson().name"></span>
<br/>
<button data-bind="click: showNext" id="btnShowNext">Show Next</button>
<br/>
</div>
//here is the JS code
function ViewModel() {
people = ko.observableArray([{
name: "Bungle"
}, {
name: "George"
}, {
name: "Zippy"
}]);
showNext = function (person) {
selectedIndex(selectedIndex() + 1);
};
selectedIndex = ko.observable(0);
selectedPerson = ko.computed(function () {
return people()[selectedIndex()];
});
}
ko.applyBindings(new ViewModel());
kindly check this jsfiddle
Create observable property for a single object, then when clicking next just set that property to other object and UI will be updated.
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});
}
});