Collapsable panel with bootstrap and knockout - javascript

I can't make an accordion with KnockoutJS, and Bootstrap to work properly. I have defined it like so:
<div class="panel-group" id="accordion" data-bind="foreach: Advertisers()">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<span data-toggle="collapse" data-bind="html: $data, attr: { 'data-target': '#' + $data }"></span>
</h4>
</div>
</div>
<div class="panel-collapse collapse" data-parent="#accordion" data-bind="attr: { id: $data }">
<div class="panel-body">
...content...
"Advertisers" is an observable array of strings, and hence $data is a string. I get one "row" for each advertiser.
All rows are initially collapsed, and clicking a row expands the content below. So far so good.
The problem is that when I click another row I would expect the previous expanded to collapse, but that's not happening. (I couldn't make a fiddle to work either, with Bootstrap and KnockoutJS...)
Edited the code.

What about a simple custom binding, which also allows you to unclutter your view a bit:
ko.bindingHandlers.bootstrapAccordion = {
init: function(elem, value, allBindings) {
var options = ko.utils.unwrapObservable(value()),
handleClass = '[data-toggle]',
contentClass = '.collapse',
openItem = ko.utils.unwrapObservable(options.openItem) || false,
itemClass = '.' + ko.utils.unwrapObservable(options.item) || '.accordion-group',
items = $(elem).find(contentClass);
// toggle: false required to hide items on load
items.collapse({ parent: elem, toggle: false });
if (openItem > -1) items.eq(openItem).collapse('show');
// if the array is dynamic, the collapse should be re-initiated to work properly
var list = allBindings.get('foreach');
if (ko.isObservable(list)) {
list.subscribe(function() {
$(elem).find(contentClass).collapse({ parent: elem, toggle: false });
});
}
$(elem).on('click', handleClass, function() {
$(elem).find(contentClass).collapse('hide');
$(this).closest(itemClass).find(contentClass).collapse('show');
});
}
};
This binding takes 2 parameters (className for container, and optionally, an item to open on load), eg: bootstrapAccordion: {item: 'panel-group', openItem: 0}, and should be set on the same element which has a foreach binding. It assumes that collapsible sections have a collapse class, and the handles to toggle them have a data-toggle attribute.
See it in action here:
http://jsfiddle.net/pkvn79h8/22/

I extended Tyblitz's example above to include support for changing an icon (eg, +/-, up/down arrow) and support for moving to the next panel by applying data-open-next attribute to whatever should move to next panel on click.
ko.bindingHandlers.bootstrapAccordion = {
init: function (elem, value, allBindings) {
var options = ko.utils.unwrapObservable(value()),
handleClass = '[data-toggle]',
contentClass = '.collapse',
openedClass = ko.utils.unwrapObservable(options.openedClass) || 'fa-minus',
closedClass = ko.utils.unwrapObservable(options.closedClass) || 'fa-plus',
openCloseToggleClasses = openedClass + ' ' + closedClass,
openItem = ko.utils.unwrapObservable(options.openItem) || false,
itemClass = '.' + (ko.utils.unwrapObservable(options.item) || 'accordion-group'),
items = $(elem).find(contentClass);
var initializeItems = function(items) {
// toggle: false required to hide items on load
items.collapse({ parent: elem, toggle: false });
if (openItem > -1) {
items.eq(openItem).collapse('show');
items.eq(openItem).closest(itemClass).find('.panel-heading').find('i').toggleClass(openCloseToggleClasses);
items.eq(openItem).closest(itemClass).find('.panel-heading').addClass('active');
}
}
initializeItems(items);
// if the array is dynamic, the collapse should be re-initiated to work properly
var list = allBindings.get('foreach');
if (ko.isObservable(list)) {
list.subscribe(function () {
initializeItems($(elem).find(contentClass));
});
}
$(elem).on('click', handleClass, function () {
$(elem).find(contentClass).collapse('hide');
$(this).closest(itemClass).find(contentClass).collapse('show');
$(this).closest(itemClass).parent().find('.panel-heading i').removeClass(openCloseToggleClasses);
$(this).closest(itemClass).parent().find('.panel-heading i').addClass(closedClass);
$(this).closest(itemClass).parent().find('.panel-heading').removeClass('active');
if ($(this).closest(itemClass).find('.panel-collapse').attr('aria-expanded') === "true") {
$(this).closest(itemClass).find('.panel-heading i').toggleClass(openCloseToggleClasses);
$(this).closest(itemClass).find('.panel-heading').addClass('active');
}
});
$(elem).on('click', '[data-open-next]', function () {
$next = $(this).closest(itemClass).next(itemClass).find(handleClass);
if ($next.length) {
$next.click();
} else {
$same = $(this).closest(itemClass).find(contentClass);
$same.collapse('hide');
$same.parent().find('.panel-heading i').removeClass(openCloseToggleClasses);
$same.parent().find('.panel-heading i').addClass(closedClass);
$same.parent().find('.panel-heading').removeClass('active');
}
});
}
};
Sample markup to use with this binding:
<div data-bind="foreach: listOfThings, bootstrapAccordion: { openItem: 0 }">
<div class="accordion-group">
<div class="panel panel-default" style="cursor: pointer;" data-toggle>
<div class="panel-heading">
<i class="fa fa-plus fa-pull-left fa-2x"></i>
<h3 data-bind="text: name">Title of expander</h3>
</div>
</div>
<div class="panel-collapse collapse">
<div class="panel-body">
<div class="clearfix" data-accordion-content>
<!-- content goes here -->
<!-- ko if: $index() < $parent.listOfThings().length -1 -->
<button data-open-next>Next Thing</button>
<!-- /ko -->
</div>
</div>
</div>
</div>
</div>
I would feel bad not contributing back :)

Related

How to click on a child element

There many items and I get object of the desired item. But I don't know how can I click on the child element in this object.
html:
<div class="item">
<div role="button" tabindex="-1">
<strong>ItemName2</strong>
</div>
<div class="d">
<div class="item-icon" role="button" tabindex="-1" style="display: none">
<i aria-label="icon: add" class="add"></i> <!-- I need to click on this Item -->
</div>
<div class="item-icon" role="button" tabindex="-1" style="display: none">
<i aria-label="icon: del" class="del"></i>
</div>
</div>
</div>
<div class="item"> ... </div>
<div class="item"> ... </div>
<div class="item"> ... </div>
js:
let fBtns = await driver.findElements(By.tagName('strong')); // Find all objects
let buttons = fBtns.map(elem => elem.getText());
const allButtons = await Promise.all(buttons);
console.log(allButtons); // All object names
let current = fBtns[fBtns.length - 1];
console.log(current); // This is desired object
await current.click(); // This is click on the object and operates as expected
// But I need to click on the <i aria-label="icon: add" class="add"> element
// How can I click on the desired element?
To click the element <i aria-label="icon: del" class="del"></i>, you can just use an XPath to query directly on the element:
await driver.findElement(By.xpath("//div[div/strong[text()='ItemName2']]/div/div/i[#class='del']")).click()
You can probably shorten this a bit to:
await driver.findElement(By.xpath("//div[div/strong[text()='ItemName2']]//i[#class='del']")).click()
Try invoking click by trigger method:
$('.item-icon .add').trigger("click");
In the below example, I scan the document for a dynamic xpath that finds the strong with string ItemName2 and then traverse back up one level (/../) before moving back down to the child element. This will act like a waitForElement that you can hopefully repurpose to trigger a click.
var MyDefaultTimeout = 1500;
var loaded = false;
do {
var icon = document.getElementsByClassName('//*[contains(#strong,\'ItemName2\')]/../div/div/i');
if(!icon.length == 0)
{
setTimeout(function() { loaded = false }, MyDefaultTimeout);
}
else
{
if(!document.readyState === 'complete')
{
setTimeout(function() { loaded = false }, MyDefaultTimeout);
}
else
{
loaded = true;
return document.readyState;
}
}
}
while(loaded === false);

How to use the $compile service to include HTML data with AngularJS directives

I have a controller which is populating content to content areas using ng-repeat. The issue is that some of this content needs to come front template files and so needs to be compiled 'on the fly'. Right now I have this function dynamically adding content:
$scope.layouts = [
{ id: 'Dashboard', icon: 'dashboard', view: '/qph/views/Dashboard.php' },
{ id: 'Customers', icon: 'people', view: '/qph/views/users.php' },
{ id: 'Quotes', icon: 'format_list_bulleted', view: '/qph/views/Quotes.php' }
];
$scope.workspace = {};
var getTemplate = function(id){
var view = 'test.php';
$timeout(function() { //added timeout
if($templateCache.get(view) === undefined) {
$templateRequest(view).then(function (data) {
$scope.workspaces.forEach(function (v) {
if (v.id == id) v.content = $compile(data)($scope);
});
});
} else {
$scope.workspaces.forEach(function (v) {
if (v.id == id) v.content = $compile($templateCache.get(view))($scope);
});
}
}, 2000);
};
$scope.workspaces =
[
{ id: 1, name: "Dashboard", icon: 'dashboard', active:true }
];
getTemplate(1);
I have checked that the data variable has the html content as expected, but the compile is outputting the following:
{"0":{"jQuery331075208394539601512":{"$scope":"$SCOPE","$ngControllerController":{}}},"length":1}
Does anyone know why its not compiling the html content as expected?
Here is the template content for reference:
<div class="col-sm-6 col-sm-offset-3" ng-controller="UserController">
<div class="col-sm-6 col-sm-offset-3">
<div class="well">
<h3>Users</h3>
<button class="btn btn-primary" style="margin-bottom: 10px" ng-click="user.getUsers()">Get Users!</button>
<ul class="list-group" ng-if="user.users">
<li class="list-group-item" ng-repeat="user in user.users">
<h4>{{user.name}}</h4>
<h5>{{user.email}}</h5>
</li>
</ul>
<div class="alert alert-danger" ng-if="user.error">
<strong>There was an error: </strong> {{user.error.error}}
<br>Please go back and login again
</div>
</div>
</div>
</div>
Here is the tabs view that is to display the compiled content:
<ul class="nav nav-tabs workspace-tabs">
<li class="nav-item" ng-repeat="space in workspaces">
<a class="nav-link" data-toggle="tab" href="#workspace{{space.id}}" ng-class="(space.active == true ) ? 'active show': ''">
<span class="hidden-sm-up"><i class="material-icons md-24">{{space.icon}}</i></span>
<span class="hidden-xs-down">{{space.name}}</span>
<button ng-click="workspace.remove($index)">x</button>
</a>
</li>
</ul>
<div class="tab-content workspace-content">
<div ng-repeat="space in workspaces" id="workspace{{space.id}}" class="tab-pane fade in" ng-class="(space.active == true ) ? 'active show': ''">
{{space.content}}
</div>
</div>
The $compile service creates a jqLite object that needs to be added to the DOM with a jqLite or jQuery append() method. Using interpolation {{ }} will only render the stringified value of the jqLite object.
<div class="tab-content workspace-content">
<div ng-repeat="space in workspaces" id="workspace{{space.id}}" class="tab-pane fade in" ng-class="(space.active == true ) ? 'active show': ''">
̶{̶{̶s̶p̶a̶c̶e̶.̶c̶o̶n̶t̶e̶n̶t̶}̶}̶
<compile html="space.html"></compile>
</div>
</div>
Instead, use a custom directive to compile and append the HTML data to the DOM:
app.directive("compile", function($compile) {
return {
link: postLink,
};
function postLink(scope, elem, attrs) {
var rawHTML = scope.$eval(attrs.html)
var linkFn = $compile(rawHTML);
var $html = linkFn(scope);
elem.append($html);
}
})
For more information, see AngularJS Developer Guide - HTML Compiler.
Use a directive.
app.directive('myCustomer', function() {
return {
templateUrl: 'test.php',
controller: 'UserController'
};
})
Template cache will be managed automatically.

Override onClick when ticking checkbox in div block

Given my onClick={() => this.activeToggle("Discharge")} in the outer most div.
Is it possible to override the onClick() when clicking on one of the two checkboxes?
This is part of a accordion and it works great, but I would like to ignore the onClick event when I check one of the two checkboxes.
EXAMPLE (bit patchy)
const stateDischarge = {
active: {
display: 'inherit'
},
inactive: {
display: 'none'
}
};
var Preferences = React.createClass({
.
.
.
activeToggle: function (panel) {
event.stopPropagation(); // <= this is there I am trying the suggestion. but not working, so trying something else.
switch (panel) {
case "Demographics":
this.setState({
activeDemographics: !this.state.activeDemographics
});
break;
case "Admission":
this.setState({
activeAdmission: !this.state.activeAdmission
});
break;
case "Discharge":
this.setState({
activeDischarge: !this.state.activeDischarge
});
break;
default:
//Statements executed when none of the values match the value of the expression
break;
}
},
.
.
.
const stateDischarge = this.state.activeDischarge ? styles.active : styles.inactive;
render: function() {
<div className="panel panel-info">
<div className="panel-heading" onClick={() => this.activeToggle("Discharge")}>
<h4 className="panel-title pull-left">
<a data-toggle="collapse" data-parent="#accordion">Discharge</a>
</h4>
<div className="pull-right">
<label className="checkbox-inline"><input type="checkbox" value=""/>Admission</label>
<label className="checkbox-inline"><input type="checkbox" value=""/>Non-Admission</label>
</div>
<div className="clearfix"></div>
</div>
<div id="collapseOne" className="panel-collapse" style={stateDischarge}>
<div className="panel-body">
<div className="col-sm-6 col-md-6">
//Content in Accordion pannel
</div>
</div>
</div>
</div>
}
});
module.exports = Preferences;

hide popover when clicked outside of it

here is my code for popover html:
<a data-placement="bottom" style="float:right;margin-right:20px;" id="new-quote-popover" popover><img src="img/user.png"/> <b>{{UserName}}</b>
<div id="popover-head" class="hide">USER DETAILS</div>
<div id="popover-content" class="hide">
<div class="row smallMargin">
<div class="col-sm-4">
varun
</div>
<div class="col-sm-8">
<select name="selectopt" style="width:80%">
<option value="001">001</option>
<option value="002">002</option>
</select>
</div>
</div>
</div>
</a>
and the directive for popover is
js:
app.directive('popover', function($compile){
return {
restrict : 'A',
link : function(scope, elem){
var content = $("#popover-content").html();
var compileContent =function() {
return $compile(content)(scope);
};
var title = $("#popover-head").html();
var options = {
content: compileContent,
html: true,
title: title
};
$(elem).popover(options);
}
}
});
It is working well..I am trying to hide the pop-over whenever cliked outside of it.But as it is in tag i am unable to do so.please help me in this.I tried with classname,id but not succeeded .
I checked this question it is not working for me
You need to inject $document service and to create new event listener mousedown on document root.
$document.on('mousedown', addClickOutsideListener);
function addClickOutsideListener(event) {
var target = event.target;
while(target != document.body) {
if(target == elem[0]) {
//user clicked on Popover, nothing to do
return;
}
if(!target.parentNode)
return;
target = target.parentNode;
}
hidePopover();
});
Don't forget add listener of scope destroying:
scope.$on('$destroy', function() {
$document.off('mousedown', addClickOutsideListener);
});

Durandal widgets, dynamic templated parts

I'm building a wizard widget with Durandal, and I'd like to use it like so:
<div data-bind="wizard: options">
<!-- Step 1 -->
<span data-part="step-header-1">
Step 1
</span>
<div data-part="step-content-1">
step content here
</div>
<!-- Step 2 -->
<span data-part="step-header-2">
Step 2
</span>
<div data-part="step-content-2">
step content here
</div>
</div>
This is the actual widget (cut down for brevity):
<div class="wizard-container">
<ul class="steps" data-bind="foreach: steps">
<li>
<span data-bind="html: heading"></span>
</li>
</ul>
<!-- ko foreach: steps -->
<div class="wizard-step" data-bind="css: { active: isActive }">
<div data-bind="html: content">
</div>
</div>
<!-- /ko -->
</div>
I've sort of gotten it working, using jQuery to grab the data-parts, assign the data-part's inner HTML to a property on my step model, and then use the html-binding to bind the content to each step. This works on the DOM side of things, but doing it this way means that my step content won't get data-bound.. I am pretty sure it's because I use the html binding, which does not bind the content.
Is there a way to do this with Durandal widgets, without separating each step into a new view?
Here's an implementation that uses a traditional Durandal master/detail approach in combination with a Tab widget. The tab widget only implements the tabbing functionality, while the Master controls what's pushed into it and the Detail controls the behavior/layout of itself.
Master
Viewmodel
define(['./tab', 'plugins/widget', 'knockout'], function (Tab, widget, ko) {
return {
tabs: ko.observableArray([
new Tab('Durandal', 'A ...', true),
new Tab('UnityDatabinding', 'A ...'),
new Tab('Caliburn.Micro', 'C ...')
]),
addNewTab: function() {
this.tabs.push(new Tab('New Tab ', 'A test tab.'));
}
};
});
View
<div>
<h1>Tabs sample</h1>
<!-- ko widget : {kind: 'tabs', items : tabs} -->
<!-- /ko -->
<button class="btn" data-bind="click: addNewTab">Add</button>
</div>
Detail
Viewmodel
define(['durandal/events', 'knockout'], function(events, ko) {
return function(name, content, isActive) {
this.isActive = ko.observable(isActive || false);
this.name = name;
this.content = content;
};
});
view
<div>
<div data-bind="html: description"></div>
</div>
Tab widget
Viewmodel
define(['durandal/composition', 'jquery'], function(composition, $) {
var ctor = function() { };
ctor.prototype.activate = function(settings) {
this.settings = settings;
};
ctor.prototype.detached = function() {
console.log('bootstrap/widget/viewmodel: detached', arguments, this);
};
ctor.prototype.toggle = function(model, event){
this.deactivateAll();
model.isActive(true);
};
ctor.prototype.deactivateAll = function(){
$.each(this.settings.items(), function(idx, tab){
tab.isActive(false);
});
};
return ctor;
});
View
<div class="tabs">
<ul class="nav nav-tabs" data-bind="foreach: { data: settings.items }">
<li data-bind="css: {active: isActive}">
<a data-bind="text: name, click: $parent.toggle.bind($parent)"></a>
</li>
</ul>
<div class="tab-content" data-bind="foreach: { data: settings.items}">
<div class="tab-pane" data-bind="html: content, css: {active: isActive}"></div>
</div>
</div>
Live version available at: http://dfiddle.github.io/dFiddle-2.0/#extras/default. Feel free to fork.
As I suspected, the problem with my bindings not applying, was due to the fact that I used the html binding to set the step content. When Knockout sets the HTML, it does not apply bindings to it.
I wrote my own HTML binding handler, that wraps the HTML and inserts it as a DOM-node - Knockout will hapily apply bindings to this.
(function(window, $, ko) {
var setHtml = function (element, valueAccessor) {
var $elem = $(element);
var unwrapped = ko.utils.unwrapObservable(valueAccessor());
var $content = $(unwrapped);
$elem.children().remove().end().append($content);
};
ko.bindingHandlers.htmlAsDom = {
init: setHtml,
update: setHtml
};
}(window, jQuery, ko));
Please note, this only works when the binding value is wrapped as a node - e.g within a div tag. If not, it won't render it.

Categories