How to use bindingHandlers for this in KnockoutJs - javascript

I'm building a mobile app which requires a bunch of open/close tab. I'm trying to find a way to use bindingHandlers to reduce the amount of code. But I seem to miss something. Here's my fiddle.
http://jsfiddle.net/noppanit/4zRrZ/
And this is what I have
<a href="javascript:void(0)" data-bind="click: expandCommentsRatings">Rating
<div style="display:none" data-bind="visible: productCommentsRatingsVisiblity">
<div class="rating" style="width: 85%">3.5 Stars Rating</div>
</div>
</a>
<br/>
<a href="javascript:void(0)" data-bind="click: expandsReviews">Reviews
<div style="display:none" data-bind="visible: productReviewsVisiblity">
<div class="reviews">Reviews</div>
</div>
</a>
var Model = function () {
var productCommentsRatingsVisiblity = ko.observable(false);
var productReviewsVisiblity = ko.observable(false);
var expandCommentsRatings = function (item, event) {
productCommentsRatingsVisiblity(!productCommentsRatingsVisiblity());
if (productCommentsRatingsVisiblity() === false) {
$(event.target).removeClass('expanded');
} else {
$(event.target).addClass('expanded');
}
};
var expandsReviews = function (item, event) {
productReviewsVisiblity(!productReviewsVisiblity());
if (productReviewsVisiblity() === false) {
$(event.target).removeClass('expanded');
} else {
$(event.target).addClass('expanded');
}
};
return {
productCommentsRatingsVisiblity: productCommentsRatingsVisiblity,
productReviewsVisiblity: productReviewsVisiblity,
expandCommentsRatings: expandCommentsRatings,
expandsReviews: expandsReviews
}
};
ko.applyBindings(Model());
How do I reduce the duplication so I can reuse this code to other ViewModel as well. The reason I'm struggling is because I don't know how to pass productCommentsRatingsVisiblity or productReviewsVisiblity to allBindings dynamically. You need to know the name in order to get it.
Thanks.

Sorry for the late reply on this, but I have a solution using bindingHandlers.
The fiddle is here: http://jsfiddle.net/u3m7m/1/
I followed a strategy of creating a toggle bindingHandler which adds the specified class if it's not present on the element, or removes the class if it is. The only state needed to make this happen is the class list on the element, meaning you can delete all those state tracking observables from the model. In fact, this was the model I used:
var Model = function () {
// stuff
};
ko.applyBindings(Model());
The toggle bindingHandler looks like this:
ko.bindingHandlers['toggle'] = {
init: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor()),
clickHandler = function (e) {
if (!e) {
e = window.event;
}
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
var classes = (this.className||'').split(' '),
index = classes.indexOf(value);
if (index >= 0) {
classes.splice(index, 1);
} else {
classes.push(value);
}
element.className = classes.join(' ');
};
element.onclick = clickHandler;
if (element.captureEvents) {
element.captureEvents(Event.CLICK);
}
}
};
Which is hopefully not too complicated, the weird looking stuff with the e object is from here: http://www.quirksmode.org/js/introevents.html
Because I'm using the strategy of using classes only, I had to add to your CSS:
.expandable > div
{
display: none;
}
.expandable.expanded > div
{
display: block;
}
The state tracking is now removed from the html, and the data-bind is modified to use the toggle bindingHandler:
<a class="expandable" href="javascript:void(0)" data-bind="toggle: 'expanded'">Rating
<div>
<div class="rating" style="width: 85%">3.5 Stars Rating</div>
</div>
</a>
<br/>
<a class="expandable" href="javascript:void(0)" data-bind="toggle: 'expanded'">Reviews
<div>
<div class="reviews">Reviews</div>
</div>
</a>
Hopefully this is of some help to you.

I'm not sure this would help you,
I've reconstruct and optimize your code based on what you need.
This might give you some idea. You don't need custom binding handler to implement this.
here the working jsFiddle: http://jsfiddle.net/farizazmi/6E4Wz/2/
so, what do you need is to include property to control visibility of the item:
var data = [
{
'name' : 'test1',
'rateIsExpanded' : ko.observable(false),
'rating': 3.5,
'review': 'blabla1',
'reviewIsExpanded': ko.observable(false)
},
{
'name' : 'test2',
'rateIsExpanded' : ko.observable(false),
'rating': 1.5,
'review': 'blabla2',
'reviewIsExpanded': ko.observable(false)
}
];
and create a function will use to change state of visibility each data:
var Model = function () {
var self = this;
self.data = ko.observableArray(data);
self.expandRate = function(item)
{
console.log(ko.toJSON(item));
item.rateIsExpanded( ! item.rateIsExpanded() );
};
self.expandReview = function(item)
{
item.reviewIsExpanded( ! item.reviewIsExpanded() );
};
};
ko.applyBindings(Model());

You can do this simply by using an observableArray to hold your menu system, with properties for:
itemName - to hold top level menu items
expanded - to control the expansion of a submenu with child items
subMenu - to hold child items
On top of this, you need a simple function to toggle the visibility of each sub-menu when the parent is clicked. Then you can utilise the knockout visible attribute in your data-binding, which would be bound to the expanded property.
Here's a working JSFiddle and below is the code used:
JS view model:
var Model = function () {
var self = this;
self.tabs = ko.observableArray([
{ itemName: "Ratings",
expanded: ko.observable(false),
subMenu: ["option 1","option 2"]},
{ itemName: "Review",
expanded: ko.observable(false),
subMenu: ["option 1","option 2"]}
]);
self.toggleExpanded = function (item) {
item.expanded(!item.expanded());
}
};
ko.applyBindings(Model());
HTML Mark Up:
<ul data-bind="foreach: tabs">
<li><span data-bind="text: itemName, click: toggleExpanded"></span>:
<ul data-bind="foreach: subMenu">
<li data-bind="text: $data, visible: $parent.expanded">
</li>
</ul>
</li>
</ul>

Related

Knockout JS - bind css class to an element

I want to achieve dynamic class biding, which should assign and reassign correct class based on variable, putted in this field. The issue I got, when I induce function setRed(), and then setWhite(), both color classes are binded, and of course 1st CSS class is considered.
I've got an element which binding looks like that:
<div data-bind='dxNumberBox: dxCustomCalc'></div>
So far I made elementAttr class ko.observable();
self.dxCustomCalc = {
displayExpr: 'Name',
keyExpr: 'Id',
value: self.customCalc,
//onValueChanged: function (e) {
// self.childFormName(e.component.option('selectedItem'));
//},
disabled: ko.computed(function () {
return self.readOnly;
}),
elementAttr: {
/* class: "is-valid-nok"*/ /* - red*/
/*class: "is-valid-onlyok" */ /* -white */
/*class: "is-required-empty",*/ /* - yellow */
class: ko.observable(),
}
};
And went through element:
function setRed() {
self.dxCustomCalc.elementAttr.class("is-valid-nok");
console.log("color changed to red")
}
function setWhite(){
self.dxCustomCalc.elementAttr.class("is-valid-onlyok");
console.log("color changed to white")
}
Functions are executed based on value in field. For example, If value matches, function setRed() is fired. Then, if the value changes and the condition is met, function setWhite() is fired.
The result I got, after executing both functions on subscription to element is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-nok is-valid-onlyok">
The result I want to achieve, after executing both functions is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-onlyok">
I'd use the class binding to set a CSS class based on an observable.
You could do use it directly
<div data-bind="dxNumberBox: dxCustomCalc, class: dxCustomCalc.cssClass">
or you could apply the class binding as part of your dxCustomCalc custom binding, using ko.applyBindingsToNode():
ko.bindingHandlers.dxNumberBox = {
init: function(elem, valueAccessor, allBindings, viewModel) {
const value = ko.unwrap(valueAccessor());
ko.applyBindingsToNode(elem, {class: value.cssClass}, viewModel);
}
};
function DxNumberBox() {
this.dxCustomCalc = {
cssClass: ko.observable("is-required-empty")
};
this.setRed = () => this.dxCustomCalc.cssClass("is-valid-nok");
this.setWhite = () => this.dxCustomCalc.cssClass("is-valid-onlyok");
this.setEmpty = () => this.dxCustomCalc.cssClass("is-required-empty");
}
const vm = new DxNumberBox();
ko.applyBindings(vm);
.is-valid-nok {
background-color: red;
}
.is-valid-onlyok {
background-color: white;
}
.is-required-empty {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<button data-bind="click: setRed">Red</button>
<button data-bind="click: setWhite">White</button>
<button data-bind="click: setEmpty">Empty</button>
<div data-bind="dxNumberBox: dxCustomCalc">
Profit Information
</div>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Functions are executed based on value in field.
Having extra methods like .setRed() is clunky and unnecessary. Turn cssClass into a computed observable that calculates a class name based on the state of the viewmodel, e.g.
cssClass: ko.pureComputed(() => {
var value = self.value().trim();
if (value == '') return 'is-required-empty';
if (value < 0) return 'is-valid-nok';
return 'is-valid-onlyok';
});

Dynamically Adding HTML files as tabs to Kendo TabStrip

I'm trying to make a web-part which is basically a Kendo tabstrip . it has a simple ul on it's own and linking external html files to each relative li using a javascript code. it works fine so far.
but now I want to be able to call functions to add or remove whatever file I selected to my tabstrip and so far it's not working .
I've searched and found some functions to the job but this one is closer to what I had in mind .
when I use the add button the tabstrip is made but the contecturl link doesn't work and it's just an empty tab.
<------------------------ web-part ------------------------>
<div class="row">
<input type='text' id='tabname' name='tabname'>
<input type='text' id='tabLink' name='tabLink'>
<input type="button" value="Add Tab" onclick="AddTab()" />
<input type="button" value="Remove Tab" onclick="closeTab()" />
</div>
<div id="tabstrip">
<ul id="tabStripUL">
<li class="k-state-active">tab1</li>
<li>tab2</li>
<li>tab3</li>
</ul>
<------------------------ Javascript ------------------------>
$(document).ready(function () {
InitLoadTabstrip();
});
function InitLoadTabstrip() {
var ts = $("#tabstrip").kendoTabStrip({
animation: { open: { effects: "fadeIn" } },
select: function(element){selecttab(element)},
contentUrls: [
'Page1.html',
'Page2.html',
'Page3.html',
]
}).data('kendoTabStrip');
}
function selecttab(element) {
var tabStrip1 = $('#tabstrip').kendoTabStrip().data("kendoTabStrip");
var item = tabStrip1.element.find("li:contains("+$(element.item).text()+")"),
itemIdx = item.index();
$("#tabstrip").data("kendoTabStrip").select(itemIdx);
}
function AddTab() {
var title = jQuery("#tabname").val();
var Address = jQuery("#tabLink").val();
var tabStrip = $("#tabstrip").kendoTabStrip().data("kendoTabStrip");
tabStrip.append({
text: title,
contentUrl: Address
});
tabStrip.select((tabStrip.tabGroup.children("li").length - 1));
}
function closeTab() {
var tabStrip = $('#tabstrip').kendoTabStrip().data("kendoTabStrip");
tabStrip.remove(tabStrip.select());
tabStrip.select((tabStrip.tabGroup.children("li").length - 1));
}
It should get a name and an Address and add that tab to the tabstrip or remove it based on the button.
I'd really appreciate it if someone could help.
<---------------------------- A quick update ----------------------------->
I tried to remove the buttons and simply add a single parameter to the addTab function to add each page that the is called . something like this :
function addTab(tabName) {
var tabStrip = $("#tabstrip").kendoTabStrip().data("kendoTabStrip");
if (tabName == "name1") {
tabStrip.append({
text: "title1",
contentUrl: 'page1.html',
});
}
else if (tabName == "name2") {
tabStrip.append({
text: "title2",
contentUrl: 'page2.html',
});
}
tabStrip.select((tabStrip.tabGroup.children("li").length - 1));
}
and call them like this :
$(document).ready(function () {
InitLoadTabstrip();
});
function InitLoadTabstrip() {
var ts = $("#tabstrip").kendoTabStrip({
animation: { open: { effects: "fadeIn" } },
select: function(element){selecttab(element)},
contentUrls: [
]
}).data('kendoTabStrip');
addTab("name1");
addTab("name2");
}
right now the problem is when I try to add more than one tab , one after the other(like the code), tabstrip sets both list items as active and it breaks the tabstrip. I think it's probably because of the 'tabstrip.select' , but I don't really understand what went wrong .
So I managed to fix it on my own , thought it may help someone else later .
the problem was that after appending I had multiple list items with "k-state-active" class that broke my tabstrip . I used jquery to manually remove the active classes whereever they were and add it up to the first li .
also I used to create a new variable each time I called addTab() instead of working on the same variable which made the whole thing alot slower and didn't have animation and select. so I made 'ts' public to be used in all the functions.
so that final code is like this :
<---------------HTML------------------>
<div id="tabstrip" style="width: 100%">
<ul id="tabStripUL">
</ul>
</div>
<----------------Script--------------->
var ts;
$(document).ready(function () {
InitLoadTabstrip();
});
function InitLoadTabstrip() {
ts = $("#tabstrip").kendoTabStrip({
animation: { open: { duration: 150, effects:"fadeIn" }
},
select: function(element){selecttab(element)},
contentUrls: [
]
}).data('kendoTabStrip');
addTab("tab1");
addTab("tab2");
}
//ts couldn't work on selecttab because of call limited size (don't really know what it is)
function selecttab(element) {
var tabStrip1 = $('#tabstrip').kendoTabStrip().data("kendoTabStrip");
var item = tabStrip1.element.find("li:contains("+$(element.item).text()+")"),
itemIdx = item.index();
$("#tabstrip").data("kendoTabStrip").select(itemIdx);
}
function addTab(tabSelect) {
if (tabSelect == "tab1") {
ts.append({
text: "title1",
contentUrl: 'page1.html',
});
//sets an id to each tab
ts.tabGroup.children().last().attr("id", "tab1");
}
else if (tabSelect == "tab2") {
ts.append({
text: "title2",
contentUrl: 'page2',
});
ts.tabGroup.children().last().attr("id", "tab2");
}
ts.select((ts.tabGroup.children("li").length - 1));
$("#tabstrip li").find(".k-state-active").removeClass("k-state-active k-tab-on-top");
$("#tabstrip li:first").addClass("k-state-active k-tab-on-top");
}
// ClearTS: clears all the tabs and contents
function clearTS() {
$("#tabstrip li").remove();
$("#tabstrip .k-content").remove();
Hope it helps !

ReactJS Cortex Object updated by render not invoked?

I am trying to implement Emberjs's Todo app as a practice exercise for Cortex by mquan on github. I am currently implementing the "All", "Active", "Completed" filter where clicking an anchor will result in the anchor being highlighted (class added).
I created the following:
var filtercortex = new cortex([
{title:'all', selected:true, key:1},
{title:'completed', selected:false, key:2},
{title:'active', selected:false, key:3}
]);
With the following render function (in the parent):
render: function() {
var filters = filterCortex.map(function(filter) {
return (
<li>
<FilterAnchor cortex={filterCortex} filter={filter} />
</li>
)
});
...
return ...
<ul id='filters'>
{filters}
</ul>
And FilterAnchor's definition:
var FilterAnchor = React.createClass({
handleClick: function() {
var that = this;
this.props.cortex.forEach(function(filter) {
if (filter.key.getValue() == that.props.filter.key.getValue()) {
console.log(filter.title.getValue(), true);
filter.selected.set(true);
} else {
console.log(filter.title.getValue(), false);
filter.selected.set(false);
}
});
return false;
},
render: function() {
var className = (this.props.filter.selected.getValue()) ? 'selected' : '';
return (
<a className={className} href="#" onClick={this.handleClick}>
{this.props.filter.title.getValue()}
</a>
)
}
});
right now, I do not see the class 'selected' being applied to the anchor links when I am clicking.
However, upon investigation I notice this:
Clicking "All":
All true
Completed false
Active false
Clicking "Completed":
All true
Completed false
Active false
So I am certain that the objects inside filtercortex has been updated properly (you can open up firebug to check). However, FilterAnchor.render is not being triggered.
Is this a bug?
Source code: https://github.com/vicngtor/ReactTodo/blob/cortex/script.jsx
The sample at the top of the Cortex readme has this at the bottom:
orderCortex.on("update", function(updatedOrder) {
orderComponent.setProps({order: updatedOrder});
});
Is there an equivalent section in your code? If not, then the problem is that the update event for the cortex data store isn't set to trigger an update of the view, which is made through a call to setProps on the top level React component in this example.

Computed Observable change does not update title attribute in UI (using knockout-bootstrap.js)

Is there anyway to trigger an update of the title attribute of my element in this JS Fiddle:
http://jsfiddle.net/YPXYJ/9/
Note that the tooltip in the data-bind attribute of the element is part of the knockout-bootstrap.js library
<label data-bind="text: copyOtherPartyHelpText()"></label>
<br />
<br />
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText(), placement: 'top', trigger: 'hover' }"></i>
<br />
<br />
<a style="cursor: pointer;" data-bind="click:changeHelpText">Click HERE To Change Label Text</a>
function MyViewModel() {
this._copyOtherPartyHelpText = ko.observable();
this.readOnlyView = ko.observable(true);
this.copyOtherPartyHelpText = ko.computed({
read: function () {
var value = this._copyOtherPartyHelpText();
if (value) {
return value;
}
if (this.readOnlyView()) {
value = 'Currently Disabled';
} else {
value = 'Match/agree to this term.';
}
//this makes things even worse, it is an initialization workaround
//_copyOtherPartyHelpText(value);
return value;
},
write: function (value) {
this._copyOtherPartyHelpText(value);
},
owner: this
});
this.changeHelpText = function(){
this.copyOtherPartyHelpText('help text updated but not tooltip');
}
}
ko.applyBindings(new MyViewModel());
The console/browser error log will tell you:
Uncaught ReferenceError: copyOtherPartyHelpText is not defined
You'd have to reference your function calls with this. or the inner function will go looking for window.copyOtherPartyHelpText instead.
I'd recommend using a local variable named self (as they often do in the knockoutjs documentation and tutorials) in your view model, so you can always reference its properties safely and easily from inside, as demonstrated in your modified JSFiddle: http://jsfiddle.net/YPXYJ/3/
function MyViewModel() {
var self = this;
// More code here...
this.changeHelpText = function(){
alert('changeHelpText called');
self.copyOtherPartyHelpText('help text and UI updated');
}
}
EDIT2:
Inside the tooltip binding for the title, you don't call the value accessor but instead reference to the observable function like so:
old:
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText(), placement: 'top', trigger: 'hover' }"></i>
new:
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText, placement: 'top', trigger: 'hover' }"></i>
See: http://jsfiddle.net/YPXYJ/11/
You needed the "this." when reffering to 'this._copyOtherPartyHelpText()' and 'this.copyOtherPartyHelpText()'
here you go http://jsfiddle.net/FtMdZ/2/
ko.observable();
this.readOnlyView = ko.observable(true);
this.copyOtherPartyHelpText = ko.computed({
read: function () {
var value = this._copyOtherPartyHelpText();
if (value) {
return value;
}
if (this.readOnlyView()) {
value = 'Currently Disabled';
} else {
value = 'Match/agree to this term.';
}
//this makes things even worse, it is an initialization workaround
//_copyOtherPartyHelpText(value);
return value;
},
write: function (value) {
this._copyOtherPartyHelpText(value);
},
owner: this
});
this.changeHelpText = function(){
alert('changeHelpText called');
this.copyOtherPartyHelpText('help text and UI updated');
}
}
ko.applyBindings(new MyViewModel());

Remove knockout js bindings on cloned element

I am using the knockout js template binding functionality to render a collection of items to an element:
<script type="text/javascript">
ko.applyBindings(new function () {
this.childItems = [{ Text: "Test", ImageUrl: "Images/Image.png" }];
});
</script>
<script type="text/html" id="template">
<div class="childItem" data-bind="attr: { title: Text }">
<img data-bind="attr: { src: ImageUrl }" />
</div>
</script>
<div class="childSelector" data-bind="template: { name: 'template', foreach: childItems }">
</div>
When clicked, the child items are cloned and placed into another element:
$(".childSelector").on("click", ".childItem", function () {
var clone = $(this).clone()[0];
ko.cleanNode(clone);
$(".targetNode").append(clone);
});
The problem is that when the source data changes and the template is re-bound to the new data, the following error is thrown:
Uncaught Error: Unable to parse bindings. Message: ReferenceError:
Text is not defined; Bindings value: attr: { title: Text }
I had found another post that suggested using ko.cleanNode(element) to remove knockout's bindings, however this has not resolved the issue in my case.
Is there a way to remove knockout's bindings on a cloned element to prevent this error when re-binding? If not I'll just "manually" clone the element by extracting the required data from the clicked element.
Here is a simple example of what I'm doing
You can remove all knockout bindings from an element by traversing the DOM and removing the data-bind attributes and knockout comments.
Use removeDataBindings(clone); but first clean the node with ko.cleanNode(clone) to clear any event handlers.
var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
function isStartComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
}
function isEndComment(node) {
return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
}
function traverseNode(node, func) {
func(node);
node = node.firstChild;
while (node) {
traverseNode(node, func);
node = node.nextSibling;
}
}
function removeDataBindings(element) {
var koComments = [];
traverseNode(element, function (node) {
if (isStartComment(node) || isEndComment(node)) {
koComments.push(node);
return;
}
//remove the 'data-bind' attributes
if (node.nodeType === 1) { //ELEMENT_NODE
node.removeAttribute('data-bind');
}
});
//remove Knockout binding comments
for (i = 0; i < koComments.length; i++) {
node = koComments[i];
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
}
Oliver, using jQuery to clone elements bound to knockout like this is not a good idea. You should be using data-binding for the targetNode. If you haven't yet done so, its a good idea to go through the Knockout Tutorials to get a good understanding of the basic uses.
If you are trying to keep a list of items, with a clone button, here is a dead simple fiddle using nothing but Knockout to do so. If you are trying to do something else, let me know; your question isn't entirely clear on your goal.
HTML:
<div data-bind="foreach: items">
<span data-bind="text: $data"></span>
<button data-bind="click: $parent.clone">Clone</button></br>
</div>
JS:
var ViewModel = function(data) {
var self = this;
self.items = ko.observableArray(data);
self.clone = function(item) {
//The ko.toJS here is a handy copy tool for viewModels
//It isn't necessary for simple arrays like this one
//But I included it because for an array of objects, you will want to use it
self.items.push(ko.toJS(item));
};
};

Categories