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>
Related
I know there must be a more efficient way of doing this, I've done it this way in the past because I haven't had many buttons to track, but I now have about 40 buttons that each update updates a mysql table with either a yes or no, and having 40 individual variables and equivalent if statements seems like bad code.
Something to note is that you can see the function has a 1 e.g. onclick='btnChange(1, this.value);. There are 7 different buttons, and then these 7 buttons repeat for onclick='btnChange(2, this.value);. So one solution I thought of is to have 7 if statements for each button and have variable names for each if statement and then I would only have to declare a lot of variables. SO I wasn't sure if that was the best way either. Does this make sense?
HTML
<button type="button" name='foo' value="bar1" onclick='btnChange(1, this.value); return false' class='form-control'>Button1</button>
<button type="button" name='hoo' value="bar2" onclick='btnChange(1, this.value); return false' class='form-control'>Button1</button>
JS
var button1YN = 0;
var button2YN = 0;
and so on...
var YNState;
function btnChange(tableid, btnID) {
if (btnID == "bar1") {
if (button1YN === 0) {
YNState = "yes";
button1YN = 1;
} else {
YNState = "no";
buttonY1N = 0;
}
}
if (btnID == "bar2") {
if (button2YN === 0) {
YNState = "yes";
button2YN = 1;
} else {
YNState = "no";
buttonY2N = 0;
}
}
//ajax code to update the mysql table
}
Instead of having a separate variable for each item, create a single variable to represent the state you're attempting to keep track of. This could be an object or an array, depending on your specific needs and/or preferences.
So you might have a state variable that looks like this for example:
// object mapping table names to on/off state
const state = {
tbl1: true,
tbl2: false,
tbl3: true
}
or an array:
const state = [true, false, true];
If you needed something more complex than true or false (on/off) you could use an array of objects:
const state = [
{
table: 't1',
on: true,
lastModified: '2021-03-23',
someOtherThing: 'foo'
},
{
table: 't2',
on: false,
lastModified: '2021-03-23',
someOtherThing: 'bananas'
},
]
Then you just need a function to update the state when something changes. For the simplest case, the true/false array, it could take the index of the item and the new value:
function updateItem(index, newValue) {
state[index] = newValue;
}
Or just toggle the existing true/false value at the given index:
const toggleItem = index => state[index] = !state[index];
Then just call that from your button click handler.
Here's a quick proof of concept:
// initial state. 7 buttons, all off (no value)
const buttons = Array.from({length: 7});
// function to flip the state at the given index
const toggleButton = index => {
buttons[index] = !buttons[index]; // negate existing value. true becomes false, vice-versa
update(); // redraw the dom
}
// redraws the html.
const update = () => {
const container = document.querySelector('.demo');
// just spitting out a button for each item in the array.
// the key thing here is the click handler, which you might
// want to register differently, but this works for
// demonstration purposes.
container.innerHTML = buttons.map((value, index) => `
<button onclick="toggleButton(${index})">
${value ? "✅" : "🔴"} Button ${index}
</button>
`).join('');
}
// do the initial draw
update();
/* purely cosmetic. irrelevant to functionality */
.demo {
display: flex;
flex-direction: column;
width: 100px;
}
button {
border: none;
padding: 0.5em;
border-radius: 4px;
margin: 0.5em;
}
<div class="demo" />
I want to remove ko observable array element by specific field value. I tried one solution. But, there are something missing. It's not working.
customOptionVal : ko.observableArray([])
customOptionVal is ko observableArray and output of that is :
Color: [0: {sub_color: "Red", sub_id: "options_3_2", is_checked: true}
1: {sub_color: "Green + $250.00", sub_id: "options_3_3", is_checked: true}]
Size: {sub_size: "L", sub_id: "options_2_2", is_checked: true}
Now, I want like that if sub_id = options_3_2 then, it will remove from Color element on the base of sub_id.
I tried this below solution. But, it's not working :
$.each(self.customOptionVal()['Color'], function( key, val ) {
if(self.customOptionVal()['Color'][key].sub_id == 'options_3_2') {
self.customOptionVal.remove(self.customOptionVal()['Color'][key]);
}
});
The following snippet removes from the customOptionVal observableArray itself -
self.customOptionVal.remove(function(option) {
return ko.utils.arrayFilter(option.Color, function(color) {
return color.sub_id === subId;
});
});
However, if you only want to remove from the Color array (which is not an observableArray), use the following snippet -
self.customOptionVal().forEach(function(option) {
var index = option["Color"].findIndex(function(y) {
return y.sub_id === subId;
});
if (index > -1) {
option["Color"].splice(index, 1);
}
});
Fiddle
I found better way that :
As see in screenshot, create one ko observable array and set Color value in that ko.observableArray
custom_option_select_text_arr = ko.observableArray([])
.....
this.custom_option_select_text_arr.push({sub_color: "Red", sub_id: "options_3_2", is_checked: true});
this.customOptionVal()['Color'] = this.custom_option_select_text_arr();
Now, for remove element :
self.custom_option_select_text_arr.remove(self.custom_option_select_text_arr()[0]);
self.customOptionVal()['Color'] = this.custom_option_select_text_arr();
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
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.
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>