I'm trying to keep track of the selected tab in the view model but I can't seem to make it work.
In the following code when you click a tab the header will update correctly but the content of the tab is not displayed. If you remove , click: $parent.selectSection then the contents are shown but the header does not update.
Now if you remove the data-bind="css: { active: selected }" from the li then it seems to work when you click the tabs but the button to select the second tab doesn't.
How can I make this work?
See: http://jsfiddle.net/5PgE2/3/
HTML:
<h3>
<span>Selected: </span>
<span data-bind="text: selectedSection().name" />
</h3>
<div class="tabbable">
<ul class="nav nav-tabs" data-bind="foreach: sections">
<li data-bind="css: { active: selected }">
<a data-bind="attr: { href: '#tab' + name }
, click: $parent.selectSection" data-toggle="tab">
<span data-bind="text: name" />
</a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: sections">
<div class="tab-pane" data-bind="attr: { id: 'tab' + name }">
<span data-bind="text: 'In section: ' + name" />
</div>
</div>
</div>
<button data-bind="click: selectTwo">Select tab Two</button>
JS:
var Section = function (name) {
this.name = name;
this.selected = ko.observable(false);
}
var ViewModel = function () {
var self = this;
self.sections = ko.observableArray([new Section('One'),
new Section('Two'),
new Section('Three')]);
self.selectedSection = ko.observable(new Section(''));
self.selectSection = function (s) {
self.selectedSection().selected(false);
self.selectedSection(s);
self.selectedSection().selected(true);
}
self.selectTwo = function() { self.selectSection(self.sections()[1]); }
}
ko.applyBindings(new ViewModel());
There are several ways that you can handle this either using bootstrap's JS or by just having Knockout add/remove the active class.
To do this just with Knockout, here is one solution where the Section itself has a computed to determine if it is currently selected.
var Section = function (name, selected) {
this.name = name;
this.isSelected = ko.computed(function() {
return this === selected();
}, this);
}
var ViewModel = function () {
var self = this;
self.selectedSection = ko.observable();
self.sections = ko.observableArray([
new Section('One', self.selectedSection),
new Section('Two', self.selectedSection),
new Section('Three', self.selectedSection)
]);
//inialize to the first section
self.selectedSection(self.sections()[0]);
}
ko.applyBindings(new ViewModel());
Markup would look like:
<div class="tabbable">
<ul class="nav nav-tabs" data-bind="foreach: sections">
<li data-bind="css: { active: isSelected }">
<a href="#" data-bind="click: $parent.selectedSection">
<span data-bind="text: name" />
</a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: sections">
<div class="tab-pane" data-bind="css: { active: isSelected }">
<span data-bind="text: 'In section: ' + name" />
</div>
</div>
</div>
Sample here: http://jsfiddle.net/rniemeyer/cGMTV/
There are a number of variations that you could use, but I think that this is a simple approach.
Here is a tweak where the active tab used the section name as a template: http://jsfiddle.net/rniemeyer/wbtvM/
Related
I am trying to toggle the plus symbol to minus sign, once after immediately that section get clicked and expanded. Following the code I have tried.
<div>
<ul style="list-style: none">
<li data-bind="foreach: model">
<div id="panelHeading">
<i class="fa fa-plus" style="padding-right: 5px;">+</i>
<span data-bind="text: Main"></span>
</div>
<div id="panelContent" data-bind="if: show">
<ul id="clustersList" data-bind="foreach: Sub" style="list-style: none">
<li><span style="padding-left: 20px;" data-bind="text: $data"></span></li>
</ul>
</div>
</li>
</ul>
</div>
=== JS ====
var viewModel = function() {
var self = this;
self.model = ko.observableArray([{
Main: "Main1",
Sub: ["hello", "hi"],
show: ko.observable(false)
}, {
Main: "Main2",
Sub: ["one", "two"],
show: ko.observable(false)
}]);
self.toggleShow = function (item) {
$('this a').find('i').toggleClass('fa fa-plus fa fa-minus');
var index = self.model.indexOf(item);
if (item.show())
self.model()[index].show(false);
else
self.model()[index].show(true);
}
}
ko.applyBindings(new viewModel());
Please check my Fiddle here.
Any suggestion would be helpful.
Just change your HTML to apply the correct style based on the current value of show:
<i class="fa" data-bind="css: { 'fa-plus': !show(), 'fa-minus': show() }"></i>
And in your JS:
self.toggleShow = function (item) {
item.show(!item.show());
};
See Fiddle
var app = angular.module("myDiscuss", []);
app.controller("TabController", function() {
this.tab = 0;
this.subTab = 0;
this.like = 0;
this.selectLike = function(setTab) {
this.like= setTab;
};
this.selectTab = function(setTab) {
this.tab= setTab;
};
this.selectSubTab = function(setTab){
this.subTab = setTab;
};
this.isSelected = (function(checkTab){
return this.tab === checkTab;
});
this.isSelectedSub = (function(checkTab){
return this.subTab === checkTab;
});
this.isSelectedLike = (function(checkTab) {
return this.like === checkTab;
});
});
app.controller('FormController', function($scope) {
$scope.person = {
name: null
};
$scope.people = [];
$scope.submit = function() {
if ($scope.person.name) {
$scope.people.push({name: $scope.person.name});
$scope.person.name = '';
}
};
});
app.directive('replyBox', function(){
return {
restrict:'A',
templateUrl:'../templates/reply-box.html'
};
});
app.directive ('commentSection', function(){
return {
restrict:'A',
scope :{},
templateUrl:'../templates/comment-section.html'
};
});
<body ng-app="myDiscuss">
<div class="container">
<div class="row">
<div>
<div class="thumbnail" ng-controller="TabController as tabs">
<div ng-show="tabs.isSelectedLike(1)">
</div>
<section class="caption">
<ul class="nav nav-pills">
<li ng-class="{ active:like === 1 }" >
<a href ng-click="tabs.selectLike(1)">Helpful</a>
</li>
<li ng-class= "{ active:tab === 2 }">
<a href ng-click="tabs.selectTab(2)">Comment</a>
</li>
</ul>
<div comment-section ng-show="tabs.isSelected(2)"></div>
</section>
</div>
</div>
</div>
</div>
</body>
<!--comment-section.html-->
<div class="panel" >
<form ng-submit="submit()" ng-controller="FormController">
<blockquote ng-repeat="(index,object) in people" >
<ul class="nav nav-pills">
<li ng-class="{ active:subTab === 3 }" >
<a href ng-click="tabs.selectSubTab(3)">Helpful</a>
</li>
<li ng-class= "{ active:subTab === 4}">
<a href ng-click="tabs.selectSubTab(4)">Reply</a>
</li>
</ul>
<div reply-box ng-show="tabs.isSelectedSub(4)"></div>
</blockquote>
<input type="text" ng-model="person.name" name="person.name" />
</form>
</div>
<!-- reply-box.html -->
<div>
<input type="text">
</div>
When I add the reply-box directive to the comment-section directive it does not perform the 'submit' action. When the "reply" link in the commentSection directive is clicked, the ng-show directive does not working for the reply-box directive.
Well I don't see any input type='submit' in your code, maybe thats why ng-submit is not working,
Moreover i think your ng-show directive isn't working because the ng-controller="TabController as tabs" ends here
<div class="thumbnail" ng-controller="TabController as tabs">
<div ng-show="tabs.isSelectedLike(1)">
</div>
<section class="caption" >
<ul class="nav nav-pills">
<li ng-class="{ active:like === 1 }" >
<a href ng-click="tabs.selectLike(1)">Helpful</a>
</li>
<li ng-class= "{ active:tab === 2 }">
<a href ng-click="tabs.selectTab(2)">Comment</a>
</li>
</ul>
<div comment-section ng-show="tabs.isSelected(2)"></div>
</section>
</div>
Thus you are calling your ng-show="tabs.isSelectedSub(4)" wont return any thing because this method is not defined in your FormController.
Hope it helps.
The errors occur because the scope for the comment section directive does not inherit from the parent scope.
Define a scope that inherits from the parent scope
To inherit from the parent scope, you'll need to set the scope property of the comment-section directive to true.
From the AngularJS documentation:
scope: true - the directive creates a new child scope that prototypically inherits from the parent scope. If more than one directive (on the same DOM element) requests a new scope, only one new child scope is created. Since we have "normal" prototypal inheritance, this is like ng-include and ng-switch, so be wary of 2-way data binding to parent scope primitives, and child scope hiding/shadowing of parent scope properties.
The comment-section directive can be rewritten thus:
app.directive ('commentSection', function(){
return {
restrict:'A',
scope :true,
templateUrl:'../templates/comment-section.html'
};
});
Background: I'm in the process of moving javascript logic in a legacy system over to knockout to get more structure and having to write less code in the future. Due to time constraints I can only move parts of the code over to knockout between each deploy.
Problem: Some of the data is generated by legacy code and some of the data is generated by knockout and I'm having problem with creating knockout logic to handle the following scenario (see below for code snippet and a JSFiddle link to the same code). The product radio buttons are not generated by knockout but the offer lists are. The key buttons to filter the offer lists works like I want them to, but I havn't managed to figure out how to get only the offers for product 1 listed under Product 1 and only offers for product 2 listed under Product 2. Anyone that could help?
I assume that if the product headers were generated from the data in the knockout view model my problem wouldn't be so hard to solve, but as you can see this isn't the case.
https://jsfiddle.net/b6er4wke/3/
function myViewModel() {
var self = this;
self.wrappedProducts = ko.observableArray();
self.availableOffers = ko.observableArray();
self.filterKey = ko.observable(1);
filteredItems = ko.computed(function() {
return ko.utils.arrayFilter(self.availableOffers(), function (offer) {
var isCorrectKey = offer.key == self.filterKey();
return (isCorrectKey);
});
});
self.filter = function(keyFilter) {
self.filterKey(keyFilter);
};
(function() {
// Products
self.wrappedProducts.push({"prod":"1"});
self.wrappedProducts.push({"prod":"2"});
// Offers
self.availableOffers.push({"name": "offer1", "key": "1", "prod": 1});
self.availableOffers.push({"name": "offer2", "key": "2", "prod": 1});
self.availableOffers.push({"name": "offer3", "key": "2", "prod": 2});
})();
}
var viewModel = new myViewModel();
ko.applyBindings(viewModel);
ul, h4 {margin-top: 0px; margin-bottom:0px;}
label {font-weight:bold;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h4>All products</h4>
<ul data-bind="foreach: wrappedProducts">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<h4>Available offers</h4>
<ul data-bind="foreach: availableOffers">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<div>
<button data-bind="click: function() {filter(1);}">key1</button>
<button data-bind="click: function() {filter(2);}">key2</button>
filterKey = <span data-bind="text: filterKey"></span>
</div>
<hr>
<label for="prod1">Product 1</label>
<ul data-bind="foreach: filteredItems">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<label for="prod2">Product 2</label>
<ul data-bind="foreach: filteredItems">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
Knockout automatically wraps your binding expressions in a computed, so you can actually simply make filteredItems a regular function and call it with the product you want to display as a parameter:
filteredItems = function filteredItems(product) {
return ko.utils.arrayFilter(self.availableOffers(), function (offer) {
return offer.key == self.filterKey() && offer.prod == product;
});
};
<label for="prod1">Product 1</label>
<ul data-bind="foreach: filteredItems(1)">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<label for="prod2">Product 2</label>
<ul data-bind="foreach: filteredItems(2)">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
and still have it update automatically when availableOffers changes.
function myViewModel() {
var self = this;
self.wrappedProducts = ko.observableArray();
self.availableOffers = ko.observableArray();
self.filterKey = ko.observable(1);
filteredItems = function filteredItems(product) {
return ko.utils.arrayFilter(self.availableOffers(), function (offer) {
return offer.key == self.filterKey() && offer.prod == product;
});
};
self.filter = function(keyFilter) {
self.filterKey(keyFilter);
};
(function() {
// Products
self.wrappedProducts.push({"prod":"1"});
self.wrappedProducts.push({"prod":"2"});
// Offers
self.availableOffers.push({"name": "offer1", "key": "1", "prod": 1});
self.availableOffers.push({"name": "offer2", "key": "2", "prod": 1});
self.availableOffers.push({"name": "offer3", "key": "2", "prod": 2});
})();
}
var viewModel = new myViewModel();
ko.applyBindings(viewModel);
ul, h4 {margin-top: 0px; margin-bottom:0px;}
label {font-weight:bold;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h4>All products</h4>
<ul data-bind="foreach: wrappedProducts">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<h4>Available offers</h4>
<ul data-bind="foreach: availableOffers">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<div>
<button data-bind="click: function() {filter(1);}">key1</button>
<button data-bind="click: function() {filter(2);}">key2</button>
filterKey = <span data-bind="text: filterKey"></span>
</div>
<hr>
<label for="prod1">Product 1</label>
<ul data-bind="foreach: filteredItems(1)">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<label for="prod2">Product 2</label>
<ul data-bind="foreach: filteredItems(2)">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
Well, The you were just displaying the results of the same computed. So, that's why under Prod 1 and Prod 2 you were getting the same results. If you want to filter by product and key, you'll need two computed functions that filter that for you.
The code you're missing inside your arrayFilter was this:
var isCorrectKey = offer.key == self.filterKey();
var isCorrectProduct = offer.prod == 2;
return isCorrectKey && isCorrectProduct;
You have to check for product and key.
function myViewModel() {
var self = this;
self.wrappedProducts = ko.observableArray();
self.availableOffers = ko.observableArray();
self.filterKey = ko.observable(1);
filteredItemsKey1 = ko.computed(function () {
return ko.utils.arrayFilter(self.availableOffers(), function (offer) {
var isCorrectKey = offer.key == self.filterKey();
var isCorrectProduct = offer.prod == 1;
return isCorrectKey && isCorrectProduct;
});
});
filteredItemsKey2 = ko.computed(function () {
return ko.utils.arrayFilter(self.availableOffers(), function (offer) {
var isCorrectKey = offer.key == self.filterKey();
var isCorrectProduct = offer.prod == 2;
return isCorrectKey && isCorrectProduct;
});
});
self.filter = function(keyFilter) {
self.filterKey(keyFilter);
};
(function() {
// Products
self.wrappedProducts.push({"prod":"1"});
self.wrappedProducts.push({"prod":"2"});
// Offers
self.availableOffers.push({"name": "offer1", "key": "1", "prod": 1});
self.availableOffers.push({"name": "offer2", "key": "2", "prod": 1});
self.availableOffers.push({"name": "offer3", "key": "2", "prod": 2});
})();
}
var viewModel = new myViewModel();
ko.applyBindings(viewModel);
ul, h4 {margin-top: 0px; margin-bottom:0px;}
label {font-weight:bold;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h4>All products</h4>
<ul data-bind="foreach: wrappedProducts">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<h4>Available offers</h4>
<ul data-bind="foreach: availableOffers">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<div>
<button data-bind="click: function() {filter(1);}">key1</button>
<button data-bind="click: function() {filter(2);}">key2</button>
filterKey = <span data-bind="text: filterKey"></span>
</div>
<hr>
<label for="prod1">Product 1</label>
<ul data-bind="foreach: filteredItemsKey1">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
<label for="prod2">Product 2</label>
<ul data-bind="foreach: filteredItemsKey2">
<li data-bind="text: ko.toJSON($data)"></li>
</ul>
I want to bind class of LI/DIV. I'm using knockout.js. I don't know how to make it works. This is my code:
<div id="users-list2" class="span8">
<div class="tabbable">
<!-- Only required for left/right tabs -->
<ul class="nav nav-tabs" data-bind="foreach: conversations">
<li data-bind="click: function () { $root.tabClick(username); }, attr:{ 'class': cls}" style="float:left">
<a class="user-box-name"
data-bind="text: username, attr:{ 'href':'#'+ username }, event: { contextmenu: $root.closeTab }"></a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: conversations">
<div data-bind="attr:{ 'id': username, 'class': 'tab-pane ' + cls}">
<div id="chat-list" class="span12" data-bind="foreach: messages">
<ul>
<li>
<div class="chat-listitem-username" data-bind="text: username">
</div>
<div class="chat-listitem-message" data-bind="html: content">
</div>
<div class="chat-listitem-timestamp" data-bind="text: timestamp.toLocaleTimeString()">
</div>
</li>
</ul>
</div>
</div>
and viewModel:
chatR.conversation = function (username) {
var self = this;
self.messages = ko.observableArray();
self.username = username;
self.test = function (x) { alert(x.username) };
self.cls = "";
}
I want to change cls to "active" when specific tab is clicked and change all others cls to "". It's not working. What am I doing wrong? Classes like "tabbable" and "nav" are defined by bootstrap.js.
EDIT:
This is how I want to chnage cls:
self.tabClick = function (username) {
self.currentConversation = username;
for (i = 0; i < self.conversations().length; i++) {
if (self.conversations()[i].username == username) {
self.conversations()[i].cls = "active";
}
else {
self.conversations()[i].cls = "";
}
}
}
EDIT2:
Changes from comment work, but I have another problem. Li has class "active" but DIC gets:
<div id="aaa_1" class="tab-pane function d(){if(0<arguments.length)
{if(!d.equalityComparer||!d.equalityComparer(c,arguments[0]))d.H(),c=arguments[0],d.G();return this}b.r.Va(d);return c}"
data-bind="attr:{ 'id': username, 'class':'tab-pane '+cls}">
What is wrong here?
Posting the details as an answer.
First Step was to make the cls property an observable and change the code to and change your code accordingly self.conversations()[i].cls("active"); or self.conversations()[i].cls("");
For the second as in edit.
You need output the value of cls and not cls as a whole
So change this
<div data-bind="attr:{ 'id': username, 'class': 'tab-pane ' + cls}">
to
<div data-bind="attr:{ 'id': username, 'class': 'tab-pane ' + cls()}">
Answer to edit 2: instead of
'tab-pane ' + cls
use
'tab-pane ' + cls()
Knockout has special binding for classes, a css binding (http://knockoutjs.com/documentation/css-binding.html).
Example:
<div data-bind='css: {classname: bla() == "something"}'>...</div>
I want have table with two columns in each <option> in me <select>.
I tried this JSFIDDLE-source. But bindings in <span> isn't working
<span data-bind="text : countryName"></span>
<span data-bind="text : selectedCountry"></span>
Any ideas? Or another solution?
EDIT
While you can't style the contents of a you can use something like Twitter's excellent Bootstrap framework for a similar effect.
http://jsfiddle.net/marrok/QdjPt/
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<span class="name" data-bind="text: selectedItem().name"></span>
<span class="desc" data-bind="text: selectedItem().desc"></span> <span class="caret"></span>
</a>
<ul class="list dropdown-menu" data-bind="foreach: items ">
<!-- I should be able to use 'click' here but I'm not sure why it doesn't work. -->
<li class="item" data-bind="event: {mouseover : function(){$root.selectedIndex($index())}}">
<span class="name" data-bind="text: name"></span>
<span class="desc" data-bind="text: desc"></span>
</li>
</ul>
</div>
Javascript:
var Thing = function(name, desc) {
var self = this;
self.name = ko.observable(name);
self.desc = ko.observable(desc);
};
var AppModel = function() {
var self = this;
self.items = ko.observableArray([
new Thing(1, "Thing One"),
new Thing(2, "Thing Two"),
new Thing(3, "Thing Three"),
new Thing(4, "Thing Four")]);
self.selectedIndex = ko.observable(0);
self.selectedItem = ko.computed(function() {
return self.items()[self.selectedIndex()];
})
};
ko.applyBindings(new AppModel());